tksbrokerapi.TKSBrokerAPI
TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios,
as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
from the console, it has a rich keys and commands, or you can use it as Python module with python import.
TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
- Open account for trading: http://tinkoff.ru/sl/AaX1Et1omnH
- TKSBrokerAPI module documentation: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
- See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
- Used constants are in the TKSEnums module: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
- About Tinkoff Invest API: https://tinkoff.github.io/investAPI/
- Tinkoff Invest API documentation: https://tinkoff.github.io/investAPI/swagger-ui/
1# -*- coding: utf-8 -*- 2# Author: Timur Gilmullin 3 4""" 5**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios, 6as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: 7from the console, it has a rich keys and commands, or you can use it as Python module with `python import`. 8 9TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive 10the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems. 11 12- **Open account for trading:** http://tinkoff.ru/sl/AaX1Et1omnH 13- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html 14- **See examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples 15- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html 16- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/ 17- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/ 18""" 19 20# Copyright (c) 2022 Gilmillin Timur Mansurovich 21# 22# Licensed under the Apache License, Version 2.0 (the "License"); 23# you may not use this file except in compliance with the License. 24# You may obtain a copy of the License at 25# 26# http://www.apache.org/licenses/LICENSE-2.0 27# 28# Unless required by applicable law or agreed to in writing, software 29# distributed under the License is distributed on an "AS IS" BASIS, 30# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 31# See the License for the specific language governing permissions and 32# limitations under the License. 33 34 35import sys 36import os 37from argparse import ArgumentParser 38from importlib.metadata import version 39 40from datetime import datetime, timedelta 41from dateutil.tz import tzlocal, tzutc 42from time import sleep 43 44import re 45import json 46import requests 47import traceback as tb 48from typing import Union 49 50from multiprocessing import cpu_count 51from multiprocessing.pool import ThreadPool 52import pandas as pd 53 54from TKSEnums import * # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/ 55 56from pricegenerator.PriceGenerator import PriceGenerator, uLogger # This module has a lot of instruments to work with candles data. See docs here: https://github.com/Tim55667757/PriceGenerator 57from pricegenerator.UniLogger import DisableLogger as PGDisLog # Method for disable log from PriceGenerator 58 59import UniLogger as uLog # Logger for TKSBrokerAPI 60 61 62# --- Common technical parameters: 63 64PGDisLog(uLogger.handlers[0]) # Disable 3-rd party logging from PriceGenerator 65uLogger = uLog.UniLogger # init logger for TKSBrokerAPI 66uLogger.level = 10 # debug level by default for TKSBrokerAPI module 67uLogger.handlers[0].level = 20 # info level by default for STDOUT of TKSBrokerAPI module 68 69__version__ = "1.5" # The "major.minor" version setup here, but build number define at the build-server only 70 71CPU_COUNT = cpu_count() # host's real CPU count 72CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1 # how many CPUs will be used for parallel calculations 73 74# --- Main constants: 75 76NANO = 0.000000001 # SI-constant nano = 10^-9 77 78 79def NanoToFloat(units: str, nano: int) -> float: 80 """ 81 Convert number in nano-view mode with string parameter `units` and integer parameter `nano` to float view. Examples: 82 83 `NanoToFloat(units="2", nano=500000000) -> 2.5` 84 85 `NanoToFloat(units="0", nano=50000000) -> 0.05` 86 87 :param units: integer string or integer parameter that represents the integer part of number 88 :param nano: integer string or integer parameter that represents the fractional part of number 89 :return: float view of number 90 """ 91 return int(units) + int(nano) * NANO 92 93 94def FloatToNano(number: float) -> dict: 95 """ 96 Convert float number to nano-type view: dictionary with string `units` and integer `nano` parameters `{"units": "string", "nano": integer}`. Examples: 97 98 `FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}` 99 100 `FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}` 101 102 :param number: float number 103 :return: nano-type view of number: `{"units": "string", "nano": integer}` 104 """ 105 splitByPoint = str(number).split(".") 106 frac = 0 107 108 if len(splitByPoint) > 1: 109 if len(splitByPoint[1]) <= 9: 110 frac = int("{}{}".format( 111 int(splitByPoint[1]), 112 "0" * (9 - len(splitByPoint[1])), 113 )) 114 115 if (number < 0) and (frac > 0): 116 frac = -frac 117 118 return {"units": str(int(number)), "nano": frac} 119 120 121def GetDatesAsString(start: str = None, end: str = None) -> tuple: 122 """ 123 Create tuple of date and time strings with timezone parsed from user-friendly date. 124 125 User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020). 126 127 Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") 128 An error exception will occur if input date has incorrect format. 129 130 If `start=None`, `end=None` then return dates from yesterday to the end of the day. 131 If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day. 132 If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`. 133 Start day may be negative integer numbers: `-1`, `-2`, `-3` - how many days ago. 134 135 Also, you can use keywords for start if `end=None`: 136 `today` (from 00:00:00 to the end of current day), 137 `yesterday` (-1 day from 00:00:00 to 23:59:59), 138 `week` (-7 day from 00:00:00 to the end of current day), 139 `month` (-30 day from 00:00:00 to the end of current day), 140 `year` (-365 day from 00:00:00 to the end of current day), 141 142 :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI. 143 See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`. 144 Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day. 145 """ 146 uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end)) 147 s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0) # start of the current day 148 e = s.replace(hour=23, minute=59, second=59, microsecond=0) # end of the current day 149 150 # time between start and the end of the current day: 151 if start is None or start.lower() == "today": 152 pass 153 154 # from start of the last day to the end of the last day: 155 elif start.lower() == "yesterday": 156 s -= timedelta(days=1) 157 e -= timedelta(days=1) 158 159 # week (-7 day from 00:00:00 to the end of the current day): 160 elif start.lower() == "week": 161 s -= timedelta(days=6) # +1 current day already taken into account 162 163 # month (-30 day from 00:00:00 to the end of current day): 164 elif start.lower() == "month": 165 s -= timedelta(days=29) # +1 current day already taken into account 166 167 # year (-365 day from 00:00:00 to the end of current day): 168 elif start.lower() == "year": 169 s -= timedelta(days=364) # +1 current day already taken into account 170 171 # -N days ago to the end of current day: 172 elif start.startswith('-') and start[1:].isdigit(): 173 s -= timedelta(days=abs(int(start)) - 1) # +1 current day already taken into account 174 175 # dates between start day at 00:00:00 and the end of the last day at 23:59:59: 176 else: 177 s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc()) 178 e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e 179 180 # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API: 181 s = s.strftime(TKS_DATE_TIME_FORMAT) 182 e = e.strftime(TKS_DATE_TIME_FORMAT) 183 184 uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e)) 185 186 return s, e 187 188 189class TinkoffBrokerServer: 190 """ 191 This class implements methods to work with Tinkoff broker server. 192 193 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 194 195 About `token`: https://tinkoff.github.io/investAPI/token/ 196 """ 197 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 198 """ 199 Main class init. 200 201 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 202 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 203 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 204 :param useCache: use default cache file with raw data to use instead of `iList`. 205 True by default. Cache is auto-update if new day has come. 206 If you don't want to use cache and always updates raw data then set `useCache=False`. 207 :param defaultCache: path to default cache file. `dump.json` by default. 208 """ 209 if token is None or not token: 210 try: 211 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 212 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 213 214 except KeyError: 215 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 216 raise Exception("Token required") 217 218 else: 219 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 220 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 221 222 if accountId is None or not accountId: 223 try: 224 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 225 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 226 227 except KeyError: 228 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 229 230 else: 231 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 232 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 233 234 self.version = __version__ # duplicate here used TKSBrokerAPI main version 235 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 236 237 Latest version: https://pypi.org/project/tksbrokerapi/ 238 """ 239 240 self.aliases = TKS_TICKER_ALIASES 241 """Some aliases instead official tickers. 242 243 See also: `TKSEnums.TKS_TICKER_ALIASES` 244 """ 245 246 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 247 248 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 249 250 self.ticker = "" 251 """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 252 253 See also: `SearchByTicker()`, `SearchInstruments()`. 254 """ 255 256 self.figi = "" 257 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. 258 259 See also: `SearchByFIGI()`, `SearchInstruments()`. 260 """ 261 262 self.depth = 1 263 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 264 265 See also: `GetCurrentPrices()`. 266 """ 267 268 self.server = r"https://invest-public-api.tinkoff.ru/rest" 269 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 270 271 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 272 """ 273 274 uLogger.debug("Broker API server: {}".format(self.server)) 275 276 self.timeout = 15 277 """Server operations timeout in seconds. Default: `15`. 278 279 See also: `SendAPIRequest()`. 280 """ 281 282 self.headers = { 283 "Content-Type": "application/json", 284 "accept": "application/json", 285 "Authorization": "Bearer {}".format(self.token), 286 "x-app-name": "Tim55667757.TKSBrokerAPI", 287 } 288 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 289 290 See also: `SendAPIRequest()`. 291 """ 292 293 self.body = None 294 """Request body which send to broker server. Default: `None`. 295 296 See also: `SendAPIRequest()`. 297 """ 298 299 self.historyFile = None 300 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 301 302 See also: `History()`. 303 """ 304 305 self.htmlHistoryFile = "index.html" 306 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 307 308 See also: `ShowHistoryChart()`. 309 """ 310 311 self.instrumentsFile = "instruments.md" 312 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 313 314 See also: `ShowInstrumentsInfo()`. 315 """ 316 317 self.searchResultsFile = "search-results.md" 318 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 319 320 See also: `SearchInstruments()`. 321 """ 322 323 self.pricesFile = "prices.md" 324 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 325 326 See also: `GetListOfPrices()`. 327 """ 328 329 self.infoFile = "info.md" 330 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 331 332 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 333 """ 334 335 self.bondsXLSXFile = "ext-bonds.xlsx" 336 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 337 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 338 339 See also: `ExtendBondsData()`. 340 """ 341 342 self.calendarFile = "calendar.md" 343 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 344 345 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 346 347 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 348 """ 349 350 self.overviewFile = "overview.md" 351 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 352 353 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 354 """ 355 356 self.overviewDigestFile = "overview-digest.md" 357 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 358 359 See also: `Overview()` with parameter `details="digest"`. 360 """ 361 362 self.overviewPositionsFile = "overview-positions.md" 363 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 364 365 See also: `Overview()` with parameter `details="positions"`. 366 """ 367 368 self.overviewOrdersFile = "overview-orders.md" 369 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 370 371 See also: `Overview()` with parameter `details="orders"`. 372 """ 373 374 self.overviewAnalyticsFile = "overview-analytics.md" 375 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 376 377 See also: `Overview()` with parameter `details="analytics"`. 378 """ 379 380 self.reportFile = "deals.md" 381 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 382 383 See also: `Deals()`. 384 """ 385 386 self.withdrawalLimitsFile = "limits.md" 387 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 388 389 See also: `OverviewLimits()` and `RequestLimits()`. 390 """ 391 392 self.userInfoFile = "user-info.md" 393 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 394 395 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 396 """ 397 398 self.userAccountsFile = "accounts.md" 399 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 400 401 See also: `OverviewAccounts()`, `RequestAccounts()`. 402 """ 403 404 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 405 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 406 407 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 408 409 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 410 """ 411 412 self.iList = None # init iList for raw instruments data 413 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 414 415 See also: `Listing()`, `DumpInstruments()`. 416 """ 417 418 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 419 if useCache: 420 if os.path.exists(self.iListDumpFile): 421 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 422 curTime = datetime.now(tzutc()) 423 424 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 425 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 426 427 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 428 429 else: 430 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 431 432 uLogger.debug("Local cache with raw instruments data is used: [{}]".format(os.path.abspath(self.iListDumpFile))) 433 uLogger.debug("Dump file was last modified [{}] UTC".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 434 435 else: 436 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 437 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 438 439 else: 440 self.iList = self.Listing() # request new raw instruments data from broker server 441 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 442 443 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 444 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 445 446 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 447 """ 448 449 @staticmethod 450 def _ParseJSON(rawData="{}", debug: bool = False) -> dict: 451 """ 452 Parse JSON from response string. 453 454 :param rawData: this is a string with JSON-formatted text. 455 :param debug: if `True` then print more debug information. 456 :return: JSON (dictionary), parsed from server response string. 457 """ 458 if debug: 459 uLogger.debug("Raw text body:") 460 uLogger.debug(rawData) 461 462 responseJSON = json.loads(rawData) if rawData else {} 463 464 if debug: 465 uLogger.debug("JSON formatted:") 466 for jsonLine in json.dumps(responseJSON, indent=4).split('\n'): 467 uLogger.debug(jsonLine) 468 469 return responseJSON 470 471 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5, debug: bool = False) -> dict: 472 """ 473 Send GET or POST request to broker server and receive JSON object. 474 475 self.header: must be defining with dictionary of headers. 476 self.body: if define then used as request body. None by default. 477 self.timeout: global request timeout, 15 seconds by default. 478 :param url: url with REST request. 479 :param reqType: send "GET" or "POST" request. "GET" by default. 480 :param retry: how many times retry after first request if an 5xx server errors occurred. 481 :param pause: sleep time in seconds between retries. 482 :param debug: if `True` then print more debug information, e.g. request and response parameters, headers etc. 483 :return: response JSON (dictionary) from broker. 484 """ 485 if reqType not in ("GET", "POST"): 486 uLogger.error("You can define request type: 'GET' or 'POST'!") 487 raise Exception("Incorrect value") 488 489 if debug: 490 uLogger.debug("Request parameters:") 491 uLogger.debug(" - REST API URL: {}".format(url)) 492 uLogger.debug(" - request type: {}".format(reqType)) 493 uLogger.debug(" - headers: {}".format(str(self.headers).replace(self.token, "*** request token ***"))) 494 uLogger.debug(" - body: {}".format(self.body)) 495 496 # fast hack to avoid all operations with some tickers/FIGI 497 responseJSON = {} 498 oK = True 499 for item in self.exclude: 500 if item in url: 501 if debug: 502 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 503 504 oK = False 505 break 506 507 if oK: 508 counter = 0 509 response = None 510 errMsg = "" 511 512 while not response and counter <= retry: 513 if reqType == "GET": 514 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 515 516 if reqType == "POST": 517 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 518 519 if debug: 520 uLogger.debug("Response:") 521 uLogger.debug(" - status code: {}".format(response.status_code)) 522 uLogger.debug(" - reason: {}".format(response.reason)) 523 uLogger.debug(" - body length: {}".format(len(response.text))) 524 uLogger.debug(" - headers: {}".format(response.headers)) 525 526 # Server returns some headers: 527 # - `x-ratelimit-limit` - shows the settings of the current user limit for this method. 528 # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute. 529 # - `x-ratelimit-reset` - time in seconds before resetting the request counter. 530 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 531 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 532 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 533 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 534 sleep(rateLimitWait) 535 536 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 537 if 400 <= response.status_code < 500: 538 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 539 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 540 counter = retry + 1 541 542 if 500 <= response.status_code < 600: 543 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 544 uLogger.debug(" - not oK, {}".format(errMsg)) 545 counter += 1 546 547 if counter <= retry: 548 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 549 sleep(pause) 550 551 responseJSON = self._ParseJSON(response.text) 552 553 if errMsg: 554 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 555 uLogger.error(" - not oK, {}".format(errMsg)) 556 557 return responseJSON 558 559 def _IUpdater(self, iType: str) -> tuple: 560 """ 561 Request instrument by type from server. See available API methods for instruments: 562 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 563 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 564 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 565 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 566 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 567 568 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 569 :return: tuple with iType name and list of available instruments of current type for defined user token. 570 """ 571 result = [] 572 573 if iType in TKS_INSTRUMENTS: 574 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 575 576 # all instruments have the same body in API v2 requests: 577 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 578 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 579 result = self.SendAPIRequest(instrumentURL, reqType="POST", debug=False)["instruments"] 580 581 return iType, result 582 583 def _IWrapper(self, kwargs): 584 """ 585 Wrapper runs instrument's update method `_IUpdater()`. 586 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 587 """ 588 return self._IUpdater(**kwargs) 589 590 def Listing(self) -> dict: 591 """ 592 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 593 594 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 595 """ 596 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 597 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 598 599 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 600 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 601 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 602 603 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 604 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 605 poolUpdater.close() 606 607 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 608 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 609 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 610 611 # calculate minimum price increment (step) for all instruments and set up instrument's type: 612 for iType in iList.keys(): 613 for ticker in iList[iType]: 614 iList[iType][ticker]["type"] = iType 615 616 if "minPriceIncrement" in iList[iType][ticker].keys(): 617 iList[iType][ticker]["step"] = NanoToFloat( 618 iList[iType][ticker]["minPriceIncrement"]["units"], 619 iList[iType][ticker]["minPriceIncrement"]["nano"], 620 ) 621 622 else: 623 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 624 625 return iList 626 627 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 628 """ 629 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 630 631 See also: `DumpInstruments()`, `Listing()`. 632 633 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 634 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 635 """ 636 if self.iListDumpFile is None or not self.iListDumpFile: 637 uLogger.error("Output name of dump file must be defined!") 638 raise Exception("Filename required") 639 640 if not self.iList or forceUpdate: 641 self.iList = self.Listing() 642 643 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 644 645 # Save as XLSX with separated sheets for every type of instruments: 646 with pd.ExcelWriter( 647 path=xlsxDumpFile, 648 date_format=TKS_DATE_FORMAT, 649 datetime_format=TKS_DATE_TIME_FORMAT, 650 mode="w", 651 ) as writer: 652 for iType in TKS_INSTRUMENTS: 653 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 654 df = df[sorted(df)] # sorted by column names 655 df = df.applymap( 656 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 657 na_action="ignore", 658 ) # converting numbers from nano-type to float in every cell 659 df.to_excel( 660 writer, 661 sheet_name=iType, 662 encoding="UTF-8", 663 freeze_panes=(1, 1), 664 ) # saving as XLSX-file with freeze first row and column as headers 665 666 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 667 668 def DumpInstruments(self, forceUpdate: bool = True) -> str: 669 """ 670 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 671 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 672 673 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 674 675 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 676 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 677 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 678 """ 679 if self.iListDumpFile is None or not self.iListDumpFile: 680 uLogger.error("Output name of dump file must be defined!") 681 raise Exception("Filename required") 682 683 if not self.iList or forceUpdate: 684 self.iList = self.Listing() 685 686 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 687 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 688 fH.write(jsonDump) 689 690 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 691 692 return jsonDump 693 694 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 695 """ 696 Show information about one instrument defined by json data and prints it in Markdown format. 697 698 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 699 700 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 701 :param show: if `True` then also printing information about instrument and its current price. 702 :return: multilines text in Markdown format with information about one instrument. 703 """ 704 splitLine = "| | |\n" 705 infoText = "" 706 707 if iJSON is not None and iJSON and isinstance(iJSON, dict): 708 info = [ 709 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 710 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 711 "| Parameters | Values |\n", 712 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 713 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 714 "| Full name: | {:<54} |\n".format(iJSON["name"]), 715 ] 716 717 if "sector" in iJSON.keys() and iJSON["sector"]: 718 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 719 720 info.append("| Country of instrument: | {:<54} |\n".format("{}{}".format( 721 "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "", 722 iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "", 723 ))) 724 725 info.extend([ 726 splitLine, 727 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 728 "| Exchange: | {:<54} |\n".format(iJSON["exchange"]), 729 ]) 730 731 if "isin" in iJSON.keys() and iJSON["isin"]: 732 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 733 734 if "classCode" in iJSON.keys(): 735 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 736 737 info.extend([ 738 splitLine, 739 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 740 splitLine, 741 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 742 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 743 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 744 ]) 745 746 if iJSON["figi"]: 747 self.figi = iJSON["figi"] 748 iJSON = iJSON | self.RequestTradingStatus() 749 750 info.extend([ 751 splitLine, 752 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 753 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 754 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 755 ]) 756 757 info.append(splitLine) 758 759 if "type" in iJSON.keys() and iJSON["type"]: 760 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 761 762 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 763 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 764 765 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 766 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 767 768 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 769 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 770 771 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 772 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 773 774 if "focusType" in iJSON.keys() and iJSON["focusType"]: 775 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 776 777 if "assetType" in iJSON.keys() and iJSON["assetType"]: 778 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 779 780 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 781 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 782 783 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 784 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 785 786 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 787 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 788 789 if "currency" in iJSON.keys(): 790 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 791 792 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 793 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 794 795 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 796 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 797 798 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 799 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 800 801 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 802 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 803 804 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 805 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 806 807 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 808 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 809 810 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 811 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 812 813 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 814 info.append("| Perpetual bond: | Yes |\n") 815 816 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 817 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 818 819 iExt = None 820 if iJSON["type"] == "Bonds": 821 info.extend([ 822 splitLine, 823 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 824 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 825 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 826 iJSON["nominal"]["currency"], 827 )), 828 ]) 829 830 if "floatingCouponFlag" in iJSON.keys(): 831 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 832 833 if "amortizationFlag" in iJSON.keys(): 834 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 835 836 info.append(splitLine) 837 838 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 839 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 840 841 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 842 843 info.extend([ 844 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 845 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 846 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 847 ]) 848 849 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 850 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 851 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 852 iJSON["aciValue"]["currency"] 853 ))) 854 855 if "currentPrice" in iJSON.keys(): 856 info.append(splitLine) 857 858 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 859 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 860 861 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 862 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 863 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 864 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 865 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 866 867 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 868 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 869 870 info.extend([ 871 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 872 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 873 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 874 )), 875 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 876 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 877 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 878 )), 879 "| Changes between last deal price and last close | {:<54} |\n".format( 880 "{:.2f}%{}".format( 881 iJSON["currentPrice"]["changes"], 882 " ({}{:.2f} {})".format( 883 "+" if bondChangesDelta > 0 else "", 884 bondChangesDelta, 885 aciCurrency 886 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 887 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 888 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 889 currency 890 ), 891 ) 892 ), 893 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 894 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 895 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 896 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 897 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 898 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 899 )), 900 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 901 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 902 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 903 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 904 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 905 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 906 )), 907 ]) 908 909 if "lot" in iJSON.keys(): 910 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 911 912 if "step" in iJSON.keys() and iJSON["step"] != 0: 913 info.append("| Minimum price increment (step): | {:<54} |\n".format(iJSON["step"])) 914 915 # Add bond payment calendar: 916 if iJSON["type"] == "Bonds": 917 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 918 info.extend(["\n", strCalendar]) 919 920 infoText += "".join(info) 921 922 if show: 923 uLogger.info("{}".format(infoText)) 924 925 else: 926 uLogger.debug("{}".format(infoText)) 927 928 if self.infoFile is not None: 929 with open(self.infoFile, "w", encoding="UTF-8") as fH: 930 fH.write(infoText) 931 932 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 933 934 return infoText 935 936 def SearchByTicker(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict: 937 """ 938 Search and return raw broker's information about instrument by its ticker. 939 `ticker` must be defined! If debug=True then print all debug messages. 940 941 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 942 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 943 :param debug: if `True` then print all debug console messages. 944 :return: JSON formatted data with information about instrument. 945 """ 946 tickerJSON = {} 947 if debug: 948 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 949 950 if not self.ticker: 951 uLogger.warning("self.ticker variable is not be empty!") 952 953 else: 954 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 955 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 956 raise Exception("Instrument not allowed") 957 958 if not self.iList: 959 self.iList = self.Listing() 960 961 if self.ticker in self.iList["Shares"].keys(): 962 tickerJSON = self.iList["Shares"][self.ticker] 963 if debug: 964 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 965 966 elif self.ticker in self.iList["Currencies"].keys(): 967 tickerJSON = self.iList["Currencies"][self.ticker] 968 if debug: 969 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 970 971 elif self.ticker in self.iList["Bonds"].keys(): 972 tickerJSON = self.iList["Bonds"][self.ticker] 973 if debug: 974 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 975 976 elif self.ticker in self.iList["Etfs"].keys(): 977 tickerJSON = self.iList["Etfs"][self.ticker] 978 if debug: 979 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 980 981 elif self.ticker in self.iList["Futures"].keys(): 982 tickerJSON = self.iList["Futures"][self.ticker] 983 if debug: 984 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 985 986 if tickerJSON: 987 self.figi = tickerJSON["figi"] 988 989 if requestPrice: 990 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 991 992 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 993 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 994 995 else: 996 tickerJSON["currentPrice"]["changes"] = 0 997 998 if show: 999 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 1000 1001 else: 1002 if show: 1003 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 1004 1005 return tickerJSON 1006 1007 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict: 1008 """ 1009 Search and return raw broker's information about instrument by its FIGI. 1010 `figi` must be defined! If debug=True then print all debug messages. 1011 1012 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1013 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1014 :param debug: if `True` then print all debug console messages. 1015 :return: JSON formatted data with information about instrument. 1016 """ 1017 figiJSON = {} 1018 if debug: 1019 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 1020 1021 if not self.figi: 1022 uLogger.warning("self.figi variable is not be empty!") 1023 1024 else: 1025 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1026 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 1027 raise Exception("Instrument not allowed") 1028 1029 if not self.iList: 1030 self.iList = self.Listing() 1031 1032 for item in self.iList["Shares"].keys(): 1033 if self.figi == self.iList["Shares"][item]["figi"]: 1034 figiJSON = self.iList["Shares"][item] 1035 1036 if debug: 1037 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 1038 1039 break 1040 1041 if not figiJSON: 1042 for item in self.iList["Currencies"].keys(): 1043 if self.figi == self.iList["Currencies"][item]["figi"]: 1044 figiJSON = self.iList["Currencies"][item] 1045 1046 if debug: 1047 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 1048 1049 break 1050 1051 if not figiJSON: 1052 for item in self.iList["Bonds"].keys(): 1053 if self.figi == self.iList["Bonds"][item]["figi"]: 1054 figiJSON = self.iList["Bonds"][item] 1055 1056 if debug: 1057 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 1058 1059 break 1060 1061 if not figiJSON: 1062 for item in self.iList["Etfs"].keys(): 1063 if self.figi == self.iList["Etfs"][item]["figi"]: 1064 figiJSON = self.iList["Etfs"][item] 1065 1066 if debug: 1067 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 1068 1069 break 1070 1071 if not figiJSON: 1072 for item in self.iList["Futures"].keys(): 1073 if self.figi == self.iList["Futures"][item]["figi"]: 1074 figiJSON = self.iList["Futures"][item] 1075 1076 if debug: 1077 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 1078 1079 break 1080 1081 if figiJSON: 1082 self.figi = figiJSON["figi"] 1083 self.ticker = figiJSON["ticker"] 1084 1085 if requestPrice: 1086 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1087 1088 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1089 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1090 1091 else: 1092 figiJSON["currentPrice"]["changes"] = 0 1093 1094 if show: 1095 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1096 1097 else: 1098 if show: 1099 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 1100 1101 return figiJSON 1102 1103 def GetCurrentPrices(self, show: bool = True) -> dict: 1104 """ 1105 Get and show Depth of Market with current prices of the instrument. If an error occurred then returns an empty record: 1106 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1107 1108 See also: 1109 1110 :param show: if `True` then print DOM to log and console. 1111 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1112 """ 1113 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1114 1115 if self.depth < 1: 1116 uLogger.error("Depth of Market (DOM) must be >=1!") 1117 raise Exception("Incorrect value") 1118 1119 if not (self.ticker or self.figi): 1120 uLogger.error("self.ticker or self.figi variables must be defined!") 1121 raise Exception("Ticker or FIGI required") 1122 1123 if self.ticker and not self.figi: 1124 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1125 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1126 1127 if not self.ticker and self.figi: 1128 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1129 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1130 1131 if not self.figi: 1132 uLogger.error("FIGI is not defined!") 1133 raise Exception("Ticker or FIGI required") 1134 1135 else: 1136 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1137 1138 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1139 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1140 self.body = str({"figi": self.figi, "depth": self.depth}) 1141 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") 1142 1143 if pricesResponse: 1144 # list of dicts with sellers orders: 1145 prices["buy"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1146 1147 # list of dicts with buyers orders: 1148 prices["sell"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1149 1150 # max price of instrument at this time: 1151 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1152 1153 # min price of instrument at this time: 1154 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1155 1156 # last price of deal with instrument: 1157 prices["lastPrice"] = NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]) if "lastPrice" in pricesResponse.keys() else 0 1158 1159 # last close price of instrument: 1160 prices["closePrice"] = NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]) if "closePrice" in pricesResponse.keys() else 0 1161 1162 else: 1163 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1164 uLogger.debug("Server response: {}".format(pricesResponse)) 1165 1166 if show: 1167 if prices["buy"] or prices["sell"]: 1168 info = [ 1169 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1170 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1171 self.ticker, 1172 self.figi, 1173 self.depth, 1174 ), 1175 uLog.sepShort, "\n", 1176 " Orders of Buyers | Orders of Sellers\n", 1177 uLog.sepShort, "\n", 1178 " Sell prices (vol.) | Buy prices (vol.)\n", 1179 uLog.sepShort, "\n", 1180 ] 1181 1182 if not prices["buy"]: 1183 info.append(" | No orders!\n") 1184 sumBuy = 0 1185 1186 else: 1187 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1188 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1189 for item in maxMinSorted: 1190 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1191 1192 if not prices["sell"]: 1193 info.append("No orders! |\n") 1194 sumSell = 0 1195 1196 else: 1197 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1198 for item in prices["sell"]: 1199 info.append("{:>19} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1200 1201 info.extend([ 1202 uLog.sepShort, "\n", 1203 "{:>19} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1204 uLog.sepShort, "\n", 1205 ]) 1206 1207 infoText = "".join(info) 1208 1209 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1210 1211 else: 1212 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1213 1214 return prices 1215 1216 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1217 """ 1218 This method get and show information about all available broker instruments for current user account. 1219 If `instrumentsFile` string is not empty then also save information to this file. 1220 1221 :param show: if `True` then print results to console, if `False` - print only to file. 1222 :return: multi-lines string with all available broker instruments 1223 """ 1224 if not self.iList: 1225 self.iList = self.Listing() 1226 1227 info = [ 1228 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1229 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1230 ] 1231 1232 # add instruments count by type: 1233 for iType in self.iList.keys(): 1234 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1235 1236 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1237 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1238 1239 # generating info tables with all instruments by type: 1240 for iType in self.iList.keys(): 1241 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1242 1243 for instrument in self.iList[iType].keys(): 1244 iName = self.iList[iType][instrument]["name"] # instrument's name 1245 if len(iName) > 57: 1246 iName = "{}...".format(iName[:54]) # right trim for a long string 1247 1248 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1249 self.iList[iType][instrument]["ticker"], 1250 iName, 1251 self.iList[iType][instrument]["figi"], 1252 self.iList[iType][instrument]["currency"], 1253 self.iList[iType][instrument]["lot"], 1254 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1255 )) 1256 1257 infoText = "".join(info) 1258 1259 if show: 1260 uLogger.info(infoText) 1261 1262 if self.instrumentsFile: 1263 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1264 fH.write(infoText) 1265 1266 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1267 1268 return infoText 1269 1270 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1271 """ 1272 This method search and show information about instruments by part of its ticker, FIGI or name. 1273 If `searchResultsFile` string is not empty then also save information to this file. 1274 1275 :param pattern: string with part of ticker, FIGI or instrument's name. 1276 :param show: if `True` then print results to console, if `False` - return list of result only. 1277 :return: list of dictionaries with all found instruments. 1278 """ 1279 if not self.iList: 1280 self.iList = self.Listing() 1281 1282 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1283 compiledPattern = re.compile(pattern, re.IGNORECASE) 1284 1285 for iType in self.iList: 1286 for instrument in self.iList[iType].values(): 1287 searchResult = compiledPattern.search(" ".join( 1288 [instrument["ticker"], instrument["figi"], instrument["name"]] 1289 )) 1290 1291 if searchResult: 1292 searchResults[iType][instrument["ticker"]] = instrument 1293 1294 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1295 info = [ 1296 "# Search results\n\n", 1297 "* **Search pattern:** [{}]\n".format(pattern), 1298 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1299 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1300 ] 1301 infoShort = info[:] 1302 1303 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1304 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1305 skippedLine = "| ... | ... | ... | ... |\n" 1306 1307 if resultsLen == 0: 1308 info.append("\nNo results\n") 1309 infoShort.append("\nNo results\n") 1310 uLogger.warning("No results. Try changing your search pattern.") 1311 1312 else: 1313 for iType in searchResults: 1314 iTypeValuesCount = len(searchResults[iType].values()) 1315 if iTypeValuesCount > 0: 1316 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1317 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1318 1319 for instrument in searchResults[iType].values(): 1320 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1321 instrument["type"], 1322 instrument["ticker"], 1323 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1324 instrument["figi"], 1325 )) 1326 1327 if iTypeValuesCount <= 5: 1328 infoShort.extend(info[-iTypeValuesCount:]) 1329 1330 else: 1331 infoShort.extend(info[-5:]) 1332 infoShort.append(skippedLine) 1333 1334 infoText = "".join(info) 1335 infoTextShort = "".join(infoShort) 1336 1337 if show: 1338 uLogger.info(infoTextShort) 1339 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1340 1341 if self.searchResultsFile: 1342 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1343 fH.write(infoText) 1344 1345 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1346 1347 return searchResults 1348 1349 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1350 """ 1351 Creating list with unique instrument FIGIs from input list of tickers or FIGIs. 1352 1353 :param instruments: list of strings with tickers or FIGIs. 1354 :return: list with unique instrument FIGIs only. 1355 """ 1356 requestedInstruments = [] 1357 for iName in instruments: 1358 if iName not in self.aliases.keys(): 1359 if iName not in requestedInstruments: 1360 requestedInstruments.append(iName) 1361 1362 else: 1363 if iName not in requestedInstruments: 1364 if self.aliases[iName] not in requestedInstruments: 1365 requestedInstruments.append(self.aliases[iName]) 1366 1367 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1368 1369 onlyUniqueFIGIs = [] 1370 for iName in requestedInstruments: 1371 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1372 continue 1373 1374 self.ticker = iName 1375 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1376 1377 if not iData: 1378 self.ticker = "" 1379 self.figi = iName 1380 1381 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1382 1383 if not iData: 1384 self.figi = "" 1385 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1386 1387 if iData and iData["figi"] not in onlyUniqueFIGIs: 1388 onlyUniqueFIGIs.append(iData["figi"]) 1389 1390 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1391 1392 return onlyUniqueFIGIs 1393 1394 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1395 """ 1396 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1397 See limits: https://tinkoff.github.io/investAPI/limits/ 1398 If `pricesFile` string is not empty then also save information to this file. 1399 1400 :param instruments: list of strings with tickers or FIGIs. 1401 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1402 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1403 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1404 """ 1405 if instruments is None or not instruments: 1406 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1407 raise Exception("Ticker or FIGI required") 1408 1409 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1410 1411 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1412 1413 iList = [] # trying to get info and current prices about all unique instruments: 1414 for self.figi in onlyUniqueFIGIs: 1415 iData = self.SearchByFIGI(requestPrice=True) 1416 iList.append(iData) 1417 1418 self.ShowListOfPrices(iList, show) 1419 1420 return iList 1421 1422 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1423 """ 1424 Show table contains current prices of given instruments. 1425 1426 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1427 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1428 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1429 :return: multilines text in Markdown format as a table contains current prices. 1430 """ 1431 infoText = "" 1432 1433 if show or self.pricesFile: 1434 info = [ 1435 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1436 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1437 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1438 ] 1439 1440 for item in iList: 1441 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1442 item["ticker"], 1443 item["figi"], 1444 item["type"], 1445 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1446 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1447 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1448 "{} / {}".format( 1449 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1450 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1451 ), 1452 "{} / {}".format( 1453 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1454 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1455 ), 1456 item["currency"], 1457 )) 1458 1459 infoText = "".join(info) 1460 1461 if show: 1462 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1463 1464 if self.pricesFile: 1465 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1466 fH.write(infoText) 1467 1468 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1469 1470 return infoText 1471 1472 def RequestTradingStatus(self) -> dict: 1473 """ 1474 Requesting trading status for the instrument defined by `figi` variable. 1475 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1476 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1477 1478 :return: dictionary with trading status attributes. Response example: 1479 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1480 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1481 """ 1482 if self.figi is None or not self.figi: 1483 uLogger.error("Variable `figi` must be defined for using this method!") 1484 raise Exception("FIGI required") 1485 1486 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1487 1488 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1489 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1490 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1491 1492 uLogger.debug("Records about current trading status successfully received") 1493 1494 return tradingStatus 1495 1496 def RequestPortfolio(self) -> dict: 1497 """ 1498 Requesting actual user's portfolio for current `accountId`. 1499 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1500 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1501 1502 :return: dictionary with user's portfolio. 1503 """ 1504 if self.accountId is None or not self.accountId: 1505 uLogger.error("Variable `accountId` must be defined for using this method!") 1506 raise Exception("Account ID required") 1507 1508 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1509 1510 self.body = str({"accountId": self.accountId}) 1511 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1512 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1513 1514 uLogger.debug("Records about user's portfolio successfully received") 1515 1516 return rawPortfolio 1517 1518 def RequestPositions(self) -> dict: 1519 """ 1520 Requesting open positions by currencies and instruments for current `accountId`. 1521 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1522 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1523 1524 :return: dictionary with open positions by instruments. 1525 """ 1526 if self.accountId is None or not self.accountId: 1527 uLogger.error("Variable `accountId` must be defined for using this method!") 1528 raise Exception("Account ID required") 1529 1530 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1531 1532 self.body = str({"accountId": self.accountId}) 1533 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1534 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1535 1536 uLogger.debug("Records about current open positions successfully received") 1537 1538 return rawPositions 1539 1540 def RequestPendingOrders(self) -> list: 1541 """ 1542 Requesting current actual pending orders for current `accountId`. 1543 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1544 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1545 1546 :return: list of dictionaries with pending orders. 1547 """ 1548 if self.accountId is None or not self.accountId: 1549 uLogger.error("Variable `accountId` must be defined for using this method!") 1550 raise Exception("Account ID required") 1551 1552 uLogger.debug("Requesting current actual pending orders. Wait, please...") 1553 1554 self.body = str({"accountId": self.accountId}) 1555 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1556 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1557 1558 uLogger.debug("[{}] records about pending orders received".format(len(rawOrders))) 1559 1560 return rawOrders 1561 1562 def RequestStopOrders(self) -> list: 1563 """ 1564 Requesting current actual stop orders for current `accountId`. 1565 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1566 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1567 1568 :return: list of dictionaries with stop orders. 1569 """ 1570 if self.accountId is None or not self.accountId: 1571 uLogger.error("Variable `accountId` must be defined for using this method!") 1572 raise Exception("Account ID required") 1573 1574 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1575 1576 self.body = str({"accountId": self.accountId}) 1577 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1578 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1579 1580 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1581 1582 return rawStopOrders 1583 1584 def Overview(self, show: bool = False, details: str = "full") -> dict: 1585 """ 1586 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1587 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1588 are defined then also save information to file. 1589 1590 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1591 many requests about the state of the portfolio, and then, based on the received data, a large number 1592 of calculation and statistics are collected. 1593 1594 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1595 :param details: how detailed should the information be? You should specify one of strings: 1596 `full` - shows full available information about portfolio status (by default), 1597 `positions` - shows only open positions, 1598 `digest` - show a short digest of the portfolio status, 1599 `analytics` - shows only the analytics section and the distribution of the portfolio by various categories, 1600 `orders` - shows only sections of open limits and stop orders. 1601 :return: dictionary with client's raw portfolio and some statistics. 1602 """ 1603 if self.accountId is None or not self.accountId: 1604 uLogger.error("Variable `accountId` must be defined for using this method!") 1605 raise Exception("Account ID required") 1606 1607 view = { 1608 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1609 "headers": {}, # list of dictionaries, response headers without "positions" section 1610 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1611 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1612 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1613 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1614 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1615 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1616 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1617 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1618 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1619 }, 1620 "stat": { # --- some statistics calculated using "raw" sections: 1621 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1622 "availableRUB": 0., # available rubles (without other currencies) 1623 "blockedRUB": 0., # blocked sum in Russian Rouble 1624 "totalChangesRUB": 0., # changes for all open trades in RUB 1625 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1626 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1627 "sharesCostRUB": 0., # costs of all shares in RUB 1628 "bondsCostRUB": 0., # costs of all bonds in RUB 1629 "etfsCostRUB": 0., # costs of all etfs in RUB 1630 "futuresCostRUB": 0., # costs of all futures in RUB 1631 "Currencies": [], # list of dictionaries of all currencies statistics 1632 "Shares": [], # list of dictionaries of all shares statistics 1633 "Bonds": [], # list of dictionaries of all bonds statistics 1634 "Etfs": [], # list of dictionaries of all etfs statistics 1635 "Futures": [], # list of dictionaries of all futures statistics 1636 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1637 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1638 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1639 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1640 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1641 }, 1642 "analytics": { # --- some analytics of portfolio: 1643 "distrByAssets": {}, # portfolio distribution by assets 1644 "distrByCompanies": {}, # portfolio distribution by companies 1645 "distrBySectors": {}, # portfolio distribution by sectors 1646 "distrByCurrencies": {}, # portfolio distribution by currencies 1647 "distrByCountries": {}, # portfolio distribution by countries 1648 } 1649 } 1650 1651 details = details.lower() 1652 availableDetails = ["full", "positions", "digest", "analytics", "orders"] 1653 if details not in availableDetails: 1654 details = "full" 1655 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1656 1657 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1658 1659 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1660 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1661 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending orders (list) 1662 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1663 1664 # save response headers without "positions" section: 1665 for key in portfolioResponse.keys(): 1666 if key != "positions": 1667 view["raw"]["headers"][key] = portfolioResponse[key] 1668 1669 else: 1670 continue 1671 1672 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1673 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1674 for item in portfolioResponse["positions"]: 1675 if item["instrumentType"] == "currency": 1676 self.figi = item["figi"] 1677 curr = self.SearchByFIGI(requestPrice=False) 1678 1679 # current price of currency in RUB: 1680 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1681 "name": curr["name"], 1682 "currentPrice": NanoToFloat( 1683 item["currentPrice"]["units"], 1684 item["currentPrice"]["nano"] 1685 ), 1686 } 1687 1688 view["raw"]["Currencies"].append(item) 1689 1690 elif item["instrumentType"] == "share": 1691 view["raw"]["Shares"].append(item) 1692 1693 elif item["instrumentType"] == "bond": 1694 view["raw"]["Bonds"].append(item) 1695 1696 elif item["instrumentType"] == "etf": 1697 view["raw"]["Etfs"].append(item) 1698 1699 elif item["instrumentType"] == "futures": 1700 view["raw"]["Futures"].append(item) 1701 1702 else: 1703 continue 1704 1705 # how many volume of currencies (by ISO currency name) are blocked: 1706 for item in view["raw"]["positions"]["blocked"]: 1707 blocked = NanoToFloat(item["units"], item["nano"]) 1708 if blocked > 0: 1709 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1710 1711 # how many volume of instruments (by FIGI) are blocked: 1712 for item in view["raw"]["positions"]["securities"]: 1713 blocked = int(item["blocked"]) 1714 if blocked > 0: 1715 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1716 1717 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1718 1719 if "rub" in allBlocked.keys(): 1720 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1721 1722 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1723 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1724 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1725 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1726 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1727 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1728 view["stat"]["portfolioCostRUB"] = sum([ 1729 view["stat"]["allCurrenciesCostRUB"], 1730 view["stat"]["sharesCostRUB"], 1731 view["stat"]["bondsCostRUB"], 1732 view["stat"]["etfsCostRUB"], 1733 view["stat"]["futuresCostRUB"], 1734 ]) 1735 1736 # --- calculating some portfolio statistics: 1737 byComp = {} # distribution by companies 1738 bySect = {} # distribution by sectors 1739 byCurr = {} # distribution by currencies (include RUB) 1740 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1741 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1742 1743 for item in portfolioResponse["positions"]: 1744 self.figi = item["figi"] 1745 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1746 1747 if instrument: 1748 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1749 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1750 1751 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1752 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1753 1754 else: 1755 blocked = 0 1756 1757 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1758 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1759 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1760 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1761 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1762 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1763 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1764 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1765 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1766 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1767 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1768 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1769 1770 statData = { 1771 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1772 "ticker": instrument["ticker"], # ticker by FIGI 1773 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1774 "volume": volume, # available volume of instrument 1775 "lots": lots, # volume in lots of instrument 1776 "direction": direction, # direction of an instrument's position: short or long 1777 "blocked": blocked, # blocked volume of currency or instrument 1778 "currentPrice": curPrice, # current instrument's price in basic asset 1779 "average": average, # current average position price 1780 "cost": cost, # current cost of all volume of instrument in basic asset 1781 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1782 "costRUB": costRUB, # cost of instrument in ruble 1783 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1784 "profit": profit, # expected profit at current moment 1785 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1786 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1787 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1788 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1789 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1790 "step": instrument["step"], # minimum price increment 1791 } 1792 1793 # adding distribution by unique countries: 1794 if statData["country"] not in byCountry.keys(): 1795 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1796 1797 else: 1798 byCountry[statData["country"]]["cost"] += costRUB 1799 byCountry[statData["country"]]["percent"] += percentCostRUB 1800 1801 if item["instrumentType"] != "currency": 1802 # adding distribution by unique companies: 1803 if statData["name"]: 1804 if statData["name"] not in byComp.keys(): 1805 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1806 1807 else: 1808 byComp[statData["name"]]["cost"] += costRUB 1809 byComp[statData["name"]]["percent"] += percentCostRUB 1810 1811 # adding distribution by unique sectors: 1812 if statData["sector"] not in bySect.keys(): 1813 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1814 1815 else: 1816 bySect[statData["sector"]]["cost"] += costRUB 1817 bySect[statData["sector"]]["percent"] += percentCostRUB 1818 1819 # adding distribution by unique currencies: 1820 if currency not in byCurr.keys(): 1821 byCurr[currency] = { 1822 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1823 "cost": costRUB, 1824 "percent": percentCostRUB 1825 } 1826 1827 else: 1828 byCurr[currency]["cost"] += costRUB 1829 byCurr[currency]["percent"] += percentCostRUB 1830 1831 # saving statistics for every instrument: 1832 if item["instrumentType"] == "currency": 1833 view["stat"]["Currencies"].append(statData) 1834 1835 # update dict with free funds for trading (total - blocked) by currencies 1836 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1837 view["stat"]["funds"][currency] = { 1838 "total": volume, 1839 "totalCostRUB": costRUB, # total volume cost in rubles 1840 "free": volume - blocked, 1841 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1842 } 1843 1844 elif item["instrumentType"] == "share": 1845 view["stat"]["Shares"].append(statData) 1846 1847 elif item["instrumentType"] == "bond": 1848 view["stat"]["Bonds"].append(statData) 1849 1850 elif item["instrumentType"] == "etf": 1851 view["stat"]["Etfs"].append(statData) 1852 1853 elif item["instrumentType"] == "Futures": 1854 view["stat"]["Futures"].append(statData) 1855 1856 else: 1857 continue 1858 1859 # total changes in Russian Ruble: 1860 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1861 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1862 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1863 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1864 view["stat"]["funds"]["rub"] = { 1865 "total": view["stat"]["availableRUB"], 1866 "totalCostRUB": view["stat"]["availableRUB"], 1867 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1868 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1869 } 1870 1871 # --- pending orders sector data: 1872 uniquePendingOrders = [] 1873 uniquePendingOrdersFIGIs = [] 1874 for item in view["raw"]["orders"]: 1875 if item["figi"] not in uniquePendingOrdersFIGIs: 1876 uniquePendingOrdersFIGIs.append(item["figi"]) 1877 uniquePendingOrders.append(item) 1878 1879 for item in uniquePendingOrders: 1880 self.figi = item["figi"] 1881 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1882 1883 if instrument: 1884 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1885 orderType = TKS_ORDER_TYPES[item["orderType"]] 1886 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1887 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1888 1889 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1890 if item["direction"] == "ORDER_DIRECTION_BUY": 1891 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1892 1893 else: 1894 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1895 1896 # requested price for order execution: 1897 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1898 1899 # necessary changes in percent to reach target from current price: 1900 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1901 1902 view["stat"]["orders"].append({ 1903 "orderID": item["orderId"], # orderId number parameter of current order 1904 "figi": item["figi"], # FIGI identification 1905 "ticker": instrument["ticker"], # ticker name by FIGI 1906 "lotsRequested": item["lotsRequested"], # requested lots value 1907 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1908 "currentPrice": lastPrice, # current instrument's price for defined action 1909 "targetPrice": target, # requested price for order execution in base currency 1910 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1911 "percentChanges": changes, # changes in percent to target from current price 1912 "currency": item["currency"], # instrument's currency name 1913 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1914 "type": orderType, # type of order from TKS_ORDER_TYPES 1915 "status": orderState, # order status from TKS_ORDER_STATES 1916 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1917 }) 1918 1919 # --- stop orders sector data: 1920 uniqueStopOrders = [] 1921 uniqueStopOrdersFIGIs = [] 1922 for item in view["raw"]["stopOrders"]: 1923 if item["figi"] not in uniqueStopOrdersFIGIs: 1924 uniqueStopOrdersFIGIs.append(item["figi"]) 1925 uniqueStopOrders.append(item) 1926 1927 for item in uniqueStopOrders: 1928 self.figi = item["figi"] 1929 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1930 1931 if instrument: 1932 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1933 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1934 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1935 1936 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1937 if "expirationTime" in item.keys(): 1938 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1939 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1940 1941 else: 1942 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1943 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1944 1945 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1946 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1947 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1948 1949 else: 1950 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1951 1952 # requested price when stop-order executed: 1953 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1954 1955 # price for limit-order, set up when stop-order executed: 1956 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1957 1958 # necessary changes in percent to reach target from current price: 1959 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1960 1961 view["stat"]["stopOrders"].append({ 1962 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1963 "figi": item["figi"], # FIGI identification 1964 "ticker": instrument["ticker"], # ticker name by FIGI 1965 "lotsRequested": item["lotsRequested"], # requested lots value 1966 "currentPrice": lastPrice, # current instrument's price for defined action 1967 "targetPrice": target, # requested price for stop-order execution in base currency 1968 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1969 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1970 "percentChanges": changes, # changes in percent to target from current price 1971 "currency": item["currency"], # instrument's currency name 1972 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1973 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 1974 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 1975 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 1976 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 1977 }) 1978 1979 # --- calculating data for analytics section: 1980 # portfolio distribution by assets: 1981 view["analytics"]["distrByAssets"] = { 1982 "Ruble": { 1983 "uniques": 1, 1984 "cost": view["stat"]["availableRUB"], 1985 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1986 }, 1987 "Currencies": { 1988 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 1989 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 1990 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1991 }, 1992 "Shares": { 1993 "uniques": len(view["stat"]["Shares"]), 1994 "cost": view["stat"]["sharesCostRUB"], 1995 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1996 }, 1997 "Bonds": { 1998 "uniques": len(view["stat"]["Bonds"]), 1999 "cost": view["stat"]["bondsCostRUB"], 2000 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2001 }, 2002 "Etfs": { 2003 "uniques": len(view["stat"]["Etfs"]), 2004 "cost": view["stat"]["etfsCostRUB"], 2005 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2006 }, 2007 "Futures": { 2008 "uniques": len(view["stat"]["Futures"]), 2009 "cost": view["stat"]["futuresCostRUB"], 2010 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2011 }, 2012 } 2013 2014 # portfolio distribution by companies: 2015 view["analytics"]["distrByCompanies"]["All money cash"] = { 2016 "ticker": "", 2017 "cost": view["stat"]["allCurrenciesCostRUB"], 2018 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2019 } 2020 view["analytics"]["distrByCompanies"].update(byComp) 2021 2022 # portfolio distribution by sectors: 2023 view["analytics"]["distrBySectors"]["All money cash"] = { 2024 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2025 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2026 } 2027 view["analytics"]["distrBySectors"].update(bySect) 2028 2029 # portfolio distribution by currencies: 2030 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2031 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2032 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2033 2034 view["analytics"]["distrByCurrencies"].update(byCurr) 2035 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2036 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2037 2038 # portfolio distribution by countries: 2039 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2040 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2041 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2042 2043 view["analytics"]["distrByCountries"].update(byCountry) 2044 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2045 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2046 2047 # --- Prepare text statistics overview in human-readable: 2048 if show: 2049 # Whatever the value `details`, header not changes: 2050 info = [ 2051 "# Client's portfolio\n\n", 2052 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 2053 "* **Account ID:** [{}]\n".format(self.accountId), 2054 ] 2055 2056 if details in ["full", "positions", "digest"]: 2057 info.extend([ 2058 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2059 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2060 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2061 view["stat"]["totalChangesRUB"], 2062 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2063 view["stat"]["totalChangesPercentRUB"], 2064 ), 2065 ]) 2066 2067 if details in ["full", "positions"]: 2068 info.extend([ 2069 "## Open positions\n\n", 2070 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2071 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2072 "| Ruble | {:>31} | | | | | |\n".format( 2073 "{:.2f} ({:.2f}) rub".format( 2074 view["stat"]["availableRUB"], 2075 view["stat"]["blockedRUB"], 2076 ) 2077 ) 2078 ]) 2079 2080 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2081 return [ 2082 "| | | | | | | |\n", 2083 "| {:<27} | | | | | {:>19} | |\n".format( 2084 noTradeStr if noTradeStr else typeStr, 2085 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2086 ), 2087 ] 2088 2089 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2090 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2091 "{} [{}]".format(data["ticker"], data["figi"]), 2092 "{:.2f} ({:.2f}) {}".format( 2093 data["volume"], 2094 data["blocked"], 2095 data["currency"], 2096 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2097 data["volume"], 2098 data["blocked"], 2099 ), 2100 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2101 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2102 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2103 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2104 "{}{:.2f} {} ({}{:.2f}%)".format( 2105 "+" if data["profit"] > 0 else "", 2106 data["profit"], data["baseCurrencyName"], 2107 "+" if data["percentProfit"] > 0 else "", 2108 data["percentProfit"], 2109 ), 2110 ) 2111 2112 # --- Show currencies section: 2113 if view["stat"]["Currencies"]: 2114 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2115 for item in view["stat"]["Currencies"]: 2116 info.append(_InfoStr(item, showCurrencyName=True)) 2117 2118 else: 2119 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2120 2121 # --- Show shares section: 2122 if view["stat"]["Shares"]: 2123 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2124 2125 for item in view["stat"]["Shares"]: 2126 info.append(_InfoStr(item)) 2127 2128 else: 2129 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2130 2131 # --- Show bonds section: 2132 if view["stat"]["Bonds"]: 2133 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2134 2135 for item in view["stat"]["Bonds"]: 2136 info.append(_InfoStr(item)) 2137 2138 else: 2139 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2140 2141 # --- Show etfs section: 2142 if view["stat"]["Etfs"]: 2143 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2144 2145 for item in view["stat"]["Etfs"]: 2146 info.append(_InfoStr(item)) 2147 2148 else: 2149 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2150 2151 # --- Show futures section: 2152 if view["stat"]["Futures"]: 2153 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2154 2155 for item in view["stat"]["Futures"]: 2156 info.append(_InfoStr(item)) 2157 2158 else: 2159 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2160 2161 if details in ["full", "orders"]: 2162 # --- Show pending orders section: 2163 if view["stat"]["orders"]: 2164 info.extend([ 2165 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2166 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2167 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2168 ]) 2169 2170 for item in view["stat"]["orders"]: 2171 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2172 "{} [{}]".format(item["ticker"], item["figi"]), 2173 item["orderID"], 2174 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2175 "{} {} ({}{:.2f}%)".format( 2176 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2177 item["baseCurrencyName"], 2178 "+" if item["percentChanges"] > 0 else "", 2179 float(item["percentChanges"]), 2180 ), 2181 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2182 item["action"], 2183 item["type"], 2184 item["date"], 2185 )) 2186 2187 else: 2188 info.append("\n## Total pending limit-orders: 0\n") 2189 2190 # --- Show stop orders section: 2191 if view["stat"]["stopOrders"]: 2192 info.extend([ 2193 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2194 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2195 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2196 ]) 2197 2198 for item in view["stat"]["stopOrders"]: 2199 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2200 "{} [{}]".format(item["ticker"], item["figi"]), 2201 item["orderID"], 2202 item["lotsRequested"], 2203 "{} {} ({}{:.2f}%)".format( 2204 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2205 item["baseCurrencyName"], 2206 "+" if item["percentChanges"] > 0 else "", 2207 float(item["percentChanges"]), 2208 ), 2209 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2210 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2211 item["action"], 2212 item["type"], 2213 item["expType"], 2214 item["createDate"], 2215 item["expDate"], 2216 )) 2217 2218 else: 2219 info.append("\n## Total stop-orders: 0\n") 2220 2221 if details in ["full", "analytics"]: 2222 # -- Show analytics section: 2223 if view["stat"]["portfolioCostRUB"] > 0: 2224 info.extend([ 2225 "\n# Analytics\n" 2226 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2227 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2228 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2229 view["stat"]["totalChangesRUB"], 2230 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2231 view["stat"]["totalChangesPercentRUB"], 2232 ), 2233 "\n## Portfolio distribution by assets\n" 2234 "\n| Type | Uniques | Percent | Current cost |\n", 2235 "|------------|---------|---------|--------------------|\n", 2236 ]) 2237 2238 for key in view["analytics"]["distrByAssets"].keys(): 2239 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2240 info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format( 2241 key, 2242 view["analytics"]["distrByAssets"][key]["uniques"], 2243 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2244 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2245 )) 2246 2247 maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()]) 2248 info.extend([ 2249 "\n## Portfolio distribution by companies\n" 2250 "\n| Company{} | Percent | Current cost |\n".format(" " * (maxLenNames - 7)), 2251 "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)), 2252 ]) 2253 2254 for company in view["analytics"]["distrByCompanies"].keys(): 2255 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2256 nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) 2257 info.append("| {} | {:<7} | {:<18} |\n".format( 2258 "{}{}{}".format( 2259 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2260 company, 2261 "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)), 2262 ), 2263 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2264 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2265 )) 2266 2267 maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()]) 2268 info.extend([ 2269 "\n## Portfolio distribution by sectors\n" 2270 "\n| Sector{} | Percent | Current cost |\n".format(" " * (maxLenSectors - 6)), 2271 "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)), 2272 ]) 2273 2274 for sector in view["analytics"]["distrBySectors"].keys(): 2275 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2276 info.append("| {}{} | {:<7} | {:<18} |\n".format( 2277 sector, 2278 "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)), 2279 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2280 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2281 )) 2282 2283 maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()]) 2284 info.extend([ 2285 "\n## Portfolio distribution by currencies\n" 2286 "\n| Instruments currencies{} | Percent | Current cost |\n".format(" " * (maxLenMoney - 22)), 2287 "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)), 2288 ]) 2289 2290 for curr in view["analytics"]["distrByCurrencies"].keys(): 2291 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2292 nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"]) 2293 info.append("| {} | {:<7} | {:<18} |\n".format( 2294 "[{}] {}{}".format( 2295 curr, 2296 view["analytics"]["distrByCurrencies"][curr]["name"], 2297 "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen), 2298 ), 2299 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2300 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2301 )) 2302 2303 maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()])) 2304 info.extend([ 2305 "\n## Portfolio distribution by countries\n" 2306 "\n| Assets by country{} | Percent | Current cost |\n".format(" " * (maxLenCountry - 17)), 2307 "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)), 2308 ]) 2309 2310 for country in view["analytics"]["distrByCountries"].keys(): 2311 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2312 nameLen = len(country) 2313 info.append("| {} | {:<7} | {:<18} |\n".format( 2314 "{}{}".format( 2315 country, 2316 "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen), 2317 ), 2318 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2319 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2320 )) 2321 2322 infoText = "".join(info) 2323 2324 uLogger.info(infoText) 2325 2326 if details == "full" and self.overviewFile: 2327 filename = self.overviewFile 2328 2329 elif details == "digest" and self.overviewDigestFile: 2330 filename = self.overviewDigestFile 2331 2332 elif details == "positions" and self.overviewPositionsFile: 2333 filename = self.overviewPositionsFile 2334 2335 elif details == "orders" and self.overviewOrdersFile: 2336 filename = self.overviewOrdersFile 2337 2338 elif details == "analytics" and self.overviewAnalyticsFile: 2339 filename = self.overviewAnalyticsFile 2340 2341 else: 2342 filename = "" 2343 2344 if filename: 2345 with open(filename, "w", encoding="UTF-8") as fH: 2346 fH.write(infoText) 2347 2348 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2349 2350 return view 2351 2352 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple: 2353 """ 2354 Returns history operations between two given dates for current `accountId`. 2355 If `reportFile` string is not empty then also save human-readable report. 2356 Shows some statistical data of closed positions. 2357 2358 :param start: see docstring in `GetDatesAsString()` method 2359 :param end: see docstring in `GetDatesAsString()` method 2360 :param show: if `True` then also prints all records to the console. 2361 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2362 :return: original list of dictionaries with history of deals records from API ("operations" key): 2363 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2364 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2365 """ 2366 if self.accountId is None or not self.accountId: 2367 uLogger.error("Variable `accountId` must be defined for using this method!") 2368 raise Exception("Account ID required") 2369 2370 startDate, endDate = GetDatesAsString(start, end) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2371 2372 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2373 2374 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2375 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2376 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2377 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2378 customStat = {} # custom statistics in additional to responseJSON 2379 2380 # --- output report in human-readable format: 2381 if show or self.reportFile: 2382 splitLine1 = "| | | | | |\n" # Summary section 2383 splitLine2 = "| | | | | | | | |\n" # Operations section 2384 nextDay = "" 2385 2386 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2387 2388 if len(ops) > 0: 2389 customStat = { 2390 "opsCount": 0, # total operations count 2391 "buyCount": 0, # buy operations 2392 "sellCount": 0, # sell operations 2393 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2394 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2395 "payIn": {"rub": 0.}, # Deposit brokerage account 2396 "payOut": {"rub": 0.}, # Withdrawals 2397 "divs": {"rub": 0.}, # Dividends income 2398 "coupons": {"rub": 0.}, # Coupon's income 2399 "brokerCom": {"rub": 0.}, # Service commissions 2400 "serviceCom": {"rub": 0.}, # Service commissions 2401 "marginCom": {"rub": 0.}, # Margin commissions 2402 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2403 } 2404 2405 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2406 for item in ops: 2407 if item["state"] == "OPERATION_STATE_EXECUTED": 2408 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2409 2410 # count buy operations: 2411 if "_BUY" in item["operationType"]: 2412 customStat["buyCount"] += 1 2413 2414 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2415 customStat["buyTotal"][item["payment"]["currency"]] += payment 2416 2417 else: 2418 customStat["buyTotal"][item["payment"]["currency"]] = payment 2419 2420 # count sell operations: 2421 elif "_SELL" in item["operationType"]: 2422 customStat["sellCount"] += 1 2423 2424 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2425 customStat["sellTotal"][item["payment"]["currency"]] += payment 2426 2427 else: 2428 customStat["sellTotal"][item["payment"]["currency"]] = payment 2429 2430 # count incoming operations: 2431 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2432 if item["payment"]["currency"] in customStat["payIn"].keys(): 2433 customStat["payIn"][item["payment"]["currency"]] += payment 2434 2435 else: 2436 customStat["payIn"][item["payment"]["currency"]] = payment 2437 2438 # count withdrawals operations: 2439 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2440 if item["payment"]["currency"] in customStat["payOut"].keys(): 2441 customStat["payOut"][item["payment"]["currency"]] += payment 2442 2443 else: 2444 customStat["payOut"][item["payment"]["currency"]] = payment 2445 2446 # count dividends income: 2447 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2448 if item["payment"]["currency"] in customStat["divs"].keys(): 2449 customStat["divs"][item["payment"]["currency"]] += payment 2450 2451 else: 2452 customStat["divs"][item["payment"]["currency"]] = payment 2453 2454 # count coupon's income: 2455 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2456 if item["payment"]["currency"] in customStat["coupons"].keys(): 2457 customStat["coupons"][item["payment"]["currency"]] += payment 2458 2459 else: 2460 customStat["coupons"][item["payment"]["currency"]] = payment 2461 2462 # count broker commissions: 2463 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2464 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2465 customStat["brokerCom"][item["payment"]["currency"]] += payment 2466 2467 else: 2468 customStat["brokerCom"][item["payment"]["currency"]] = payment 2469 2470 # count service commissions: 2471 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2472 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2473 customStat["serviceCom"][item["payment"]["currency"]] += payment 2474 2475 else: 2476 customStat["serviceCom"][item["payment"]["currency"]] = payment 2477 2478 # count margin commissions: 2479 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2480 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2481 customStat["marginCom"][item["payment"]["currency"]] += payment 2482 2483 else: 2484 customStat["marginCom"][item["payment"]["currency"]] = payment 2485 2486 # count withholding taxes: 2487 elif "_TAX" in item["operationType"]: 2488 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2489 customStat["allTaxes"][item["payment"]["currency"]] += payment 2490 2491 else: 2492 customStat["allTaxes"][item["payment"]["currency"]] = payment 2493 2494 else: 2495 continue 2496 2497 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2498 2499 # --- view "Actions" lines: 2500 info.extend([ 2501 "| 1 | 2 | 3 | 4 | 5 |\n", 2502 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2503 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2504 "| | Buy: {:<22} | {:<28} | | |\n".format( 2505 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2506 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2507 ), 2508 "| | Sell: {:<21} | {:<28} | | |\n".format( 2509 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2510 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2511 ), 2512 ]) 2513 2514 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2515 for key in opsKeys: 2516 if key == "rub": 2517 continue 2518 2519 info.extend([ 2520 "| | | {:<28} | | |\n".format( 2521 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2522 ), 2523 "| | | {:<28} | | |\n".format( 2524 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2525 ), 2526 ]) 2527 2528 info.append(splitLine1) 2529 2530 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2531 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2532 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2533 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2534 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2535 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2536 ) 2537 2538 # --- view "Payments" lines: 2539 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2540 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2541 2542 for key in paymentsKeys: 2543 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2544 2545 info.append(splitLine1) 2546 2547 # --- view "Commissions and taxes" lines: 2548 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2549 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2550 2551 for key in comKeys: 2552 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2553 2554 info.append(splitLine1) 2555 2556 info.extend([ 2557 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2558 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2559 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2560 ]) 2561 2562 else: 2563 info.append("Broker returned no operations during this period\n") 2564 2565 # --- view "Operations" section: 2566 for item in ops: 2567 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2568 continue 2569 2570 else: 2571 self.figi = item["figi"] if item["figi"] else "" 2572 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2573 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2574 2575 # group of deals during one day: 2576 if nextDay and item["date"].split("T")[0] != nextDay: 2577 info.append(splitLine2) 2578 nextDay = "" 2579 2580 else: 2581 nextDay = item["date"].split("T")[0] # saving current day for splitting 2582 2583 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2584 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2585 self.figi if self.figi else "—", 2586 instrument["ticker"] if instrument else "—", 2587 instrument["type"] if instrument else "—", 2588 item["quantity"] if int(item["quantity"]) > 0 else "—", 2589 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2590 TKS_OPERATION_STATES[item["state"]], 2591 TKS_OPERATION_TYPES[item["operationType"]], 2592 )) 2593 2594 infoText = "".join(info) 2595 2596 if show: 2597 uLogger.info(infoText) 2598 2599 if self.reportFile: 2600 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2601 fH.write(infoText) 2602 2603 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2604 2605 return ops, customStat 2606 2607 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2608 """ 2609 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2610 2611 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2612 Warning! Broker server used ISO UTC time by default. 2613 2614 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2615 Also, `historyFile` used to update history with `onlyMissing` parameter. 2616 2617 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2618 2619 :param start: see docstring in `GetDatesAsString()` method. 2620 :param end: see docstring in `GetDatesAsString()` method. 2621 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2622 `"hour"`, `"day"`. Default: `"hour"`. 2623 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2624 False by default. Warning! History appends only from last candle to current time 2625 with always update last candle! 2626 :param csvSep: separator if csv-file is used, `,` by default. 2627 :param show: if `True` then also prints Pandas DataFrame to the console. 2628 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2629 `["date", "time", "open", "high", "low", "close", "volume"]`. 2630 """ 2631 strStartDate, strEndDate = GetDatesAsString(start, end) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2632 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2633 history = None # empty pandas object for history 2634 2635 if interval not in TKS_CANDLE_INTERVALS.keys(): 2636 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2637 raise Exception("Incorrect value") 2638 2639 if not (self.ticker or self.figi): 2640 uLogger.error("Ticker or FIGI must be defined!") 2641 raise Exception("Ticker or FIGI required") 2642 2643 if self.ticker and not self.figi: 2644 instrumentByTicker = self.SearchByTicker(requestPrice=False, debug=False) 2645 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2646 2647 if self.figi and not self.ticker: 2648 instrumentByFIGI = self.SearchByFIGI(requestPrice=False, debug=False) 2649 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2650 2651 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2652 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2653 if interval.lower() != "day": 2654 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59 2655 2656 delta = dtEnd - dtStart # current UTC time minus last time in file 2657 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2658 2659 # calculate history length in candles: 2660 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2661 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2662 length += 1 # to avoid fraction time 2663 2664 # calculate data blocks count: 2665 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2666 2667 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2668 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2669 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2670 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2671 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2672 2673 tempOld = None # pandas object for old history, if --only-missing key present 2674 lastTime = None # datetime object of last old candle in file 2675 2676 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2677 uLogger.debug("--only-missing key present, add only last missing candles...") 2678 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2679 2680 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2681 2682 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2683 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2684 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2685 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2686 2687 # get last datetime object from last string in file or minus 1 delta if file is empty: 2688 if len(tempOld) > 0: 2689 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2690 2691 else: 2692 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2693 2694 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2695 2696 responseJSONs = [] # raw history blocks of data 2697 2698 blockEnd = dtEnd 2699 for item in range(blocks): 2700 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2701 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2702 2703 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2704 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2705 )) 2706 2707 if blockStart == blockEnd: 2708 uLogger.debug("Skipped this zero-length block...") 2709 2710 else: 2711 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2712 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2713 self.body = str({ 2714 "figi": self.figi, 2715 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2716 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2717 "interval": TKS_CANDLE_INTERVALS[interval][0] 2718 }) 2719 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1, debug=False) 2720 2721 if "code" in responseJSON.keys(): 2722 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2723 2724 else: 2725 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2726 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2727 2728 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2729 2730 blockEnd = blockStart 2731 2732 printCount = len(responseJSONs) # candles to show in console 2733 if responseJSONs: 2734 tempHistory = pd.DataFrame( 2735 data={ 2736 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2737 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2738 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2739 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2740 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2741 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2742 "volume": [int(item["volume"]) for item in responseJSONs], 2743 }, 2744 index=range(len(responseJSONs)), 2745 columns=["date", "time", "open", "high", "low", "close", "volume"], 2746 ) 2747 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2748 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2749 2750 # append only newest candles to old history if --only-missing key present: 2751 if onlyMissing and tempOld is not None and lastTime is not None: 2752 index = 0 # find start index in tempHistory data: 2753 2754 for i, item in tempHistory.iterrows(): 2755 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2756 2757 if curTime == lastTime: 2758 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2759 index = i 2760 printCount = index + 1 2761 break 2762 2763 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2764 2765 else: 2766 history = tempHistory # if no `--only-missing` key then load full data from server 2767 2768 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2769 2770 if history is not None and not history.empty: 2771 if show: 2772 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2773 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2774 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2775 )) 2776 2777 else: 2778 uLogger.warning("Received an empty candles history!") 2779 2780 if self.historyFile is not None: 2781 if history is not None and not history.empty: 2782 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2783 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2784 2785 else: 2786 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2787 2788 else: 2789 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2790 2791 return history 2792 2793 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2794 """ 2795 Load candles history from csv-file and return Pandas DataFrame object. 2796 2797 See also: `History()` and `ShowHistoryChart()` methods. 2798 2799 :param filePath: path to csv-file to open. 2800 """ 2801 loadedHistory = None # init candles data object 2802 2803 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2804 2805 if os.path.exists(filePath): 2806 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2807 2808 tfStr = self.priceModel.FormattedDelta( 2809 self.priceModel.timeframe, 2810 "{days} days {hours}h {minutes}m {seconds}s", 2811 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2812 self.priceModel.timeframe, 2813 "{hours}h {minutes}m {seconds}s", 2814 ) 2815 2816 if loadedHistory is not None and not loadedHistory.empty: 2817 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2818 len(loadedHistory), 2819 tfStr, 2820 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2821 ) 2822 2823 else: 2824 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2825 2826 else: 2827 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2828 2829 return loadedHistory 2830 2831 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2832 """ 2833 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2834 2835 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2836 Default: `index.html` (both for interact and non-interact candlesticks chart). 2837 2838 See also: `History()` and `LoadHistory()` methods. 2839 2840 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2841 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2842 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2843 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2844 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2845 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2846 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2847 """ 2848 if isinstance(candles, str): 2849 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2850 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2851 2852 elif isinstance(candles, pd.DataFrame): 2853 self.priceModel.prices = candles # set candles chain from variable 2854 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2855 2856 if "datetime" not in candles.columns: 2857 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2858 2859 else: 2860 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2861 raise Exception("Incorrect value") 2862 2863 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2864 2865 if interact: 2866 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2867 2868 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2869 2870 else: 2871 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2872 2873 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2874 2875 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2876 2877 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2878 """ 2879 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2880 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2881 2882 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2883 2884 :param operation: string "Buy" or "Sell". 2885 :param lots: volume, integer count of lots >= 1. 2886 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2887 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2888 :param expDate: string "Undefined" by default or local date in future, 2889 it is a string with format `%Y-%m-%d %H:%M:%S`. 2890 :return: JSON with response from broker server. 2891 """ 2892 if self.accountId is None or not self.accountId: 2893 uLogger.error("Variable `accountId` must be defined for using this method!") 2894 raise Exception("Account ID required") 2895 2896 if operation is None or not operation or operation not in ("Buy", "Sell"): 2897 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2898 raise Exception("Incorrect value") 2899 2900 if lots is None or lots < 1: 2901 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2902 lots = 1 2903 2904 if tp is None or tp < 0: 2905 tp = 0 2906 2907 if sl is None or sl < 0: 2908 sl = 0 2909 2910 if expDate is None or not expDate: 2911 expDate = "Undefined" 2912 2913 if not (self.ticker or self.figi): 2914 uLogger.error("Ticker or FIGI must be defined!") 2915 raise Exception("Ticker or FIGI required") 2916 2917 instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False) 2918 self.ticker = instrument["ticker"] 2919 self.figi = instrument["figi"] 2920 2921 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2922 2923 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2924 self.body = str({ 2925 "figi": self.figi, 2926 "quantity": str(lots), 2927 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2928 "accountId": str(self.accountId), 2929 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2930 }) 2931 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0, debug=False) 2932 2933 if "orderId" in response.keys(): 2934 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2935 operation, response["orderId"], 2936 self.ticker, self.figi, lots, 2937 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2938 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2939 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2940 )) 2941 2942 else: 2943 uLogger.warning("Not `oK` status received! Market order not created. See full debug log or try again and open order later.") 2944 2945 if tp > 0: 2946 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2947 2948 if sl > 0: 2949 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2950 2951 return response 2952 2953 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2954 """ 2955 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2956 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 2957 2958 See also: `Order()` and `Trade()` docstrings. 2959 2960 :param lots: volume, integer count of lots >= 1. 2961 :param tp: float > 0, take profit price of stop-order. 2962 :param sl: float > 0, stop loss price of stop-order. 2963 :param expDate: it's a local date in future. 2964 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2965 :return: JSON with response from broker server. 2966 """ 2967 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 2968 2969 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2970 """ 2971 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 2972 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2973 2974 See also: `Order()` and `Trade()` docstrings. 2975 2976 :param lots: volume, integer count of lots >= 1. 2977 :param tp: float > 0, take profit price of stop-order. 2978 :param sl: float > 0, stop loss price of stop-order. 2979 :param expDate: it's a local date in the future. 2980 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2981 :return: JSON with response from broker server. 2982 """ 2983 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 2984 2985 def CloseTrades(self, tickers: list, portfolio: dict = None) -> None: 2986 """ 2987 Close position of given instruments. 2988 2989 :param tickers: tickers list of instruments that must be closed. 2990 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 2991 This avoids unnecessary downloading data from the server. 2992 """ 2993 if not tickers: 2994 uLogger.info("Tickers list is empty, nothing to close.") 2995 2996 else: 2997 if portfolio is None or not portfolio: 2998 portfolio = self.Overview(show=False) 2999 3000 allOpenedTickers = [item["ticker"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3001 uLogger.debug("All opened instruments by it's tickers names: {}".format(allOpenedTickers)) 3002 3003 for ticker in tickers: 3004 if ticker not in allOpenedTickers: 3005 uLogger.warning("Instrument with ticker [{}] not in open positions list!".format(ticker)) 3006 continue 3007 3008 # search open trade info about instrument by ticker: 3009 instrument = {} 3010 for iType in TKS_INSTRUMENTS: 3011 if instrument: 3012 break 3013 3014 for item in portfolio["stat"][iType]: 3015 if item["ticker"] == ticker: 3016 instrument = item 3017 break 3018 3019 if instrument: 3020 self.ticker = ticker 3021 self.figi = instrument["figi"] 3022 3023 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3024 self.ticker, 3025 self.figi, 3026 int(instrument["volume"]), 3027 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3028 )) 3029 3030 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3031 3032 if tradeLots > 0: 3033 if instrument["blocked"] > 0: 3034 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3035 instrument["blocked"], 3036 self.ticker, 3037 tradeLots, 3038 )) 3039 3040 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3041 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3042 3043 else: 3044 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker)) 3045 3046 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3047 """ 3048 Close all positions of given instruments with defined type. 3049 3050 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3051 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3052 This avoids unnecessary downloading data from the server. 3053 """ 3054 if iType not in TKS_INSTRUMENTS: 3055 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3056 3057 else: 3058 if portfolio is None or not portfolio: 3059 portfolio = self.Overview(show=False) 3060 3061 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3062 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3063 3064 if tickers and portfolio: 3065 self.CloseTrades(tickers, portfolio) 3066 3067 else: 3068 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3069 3070 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3071 """ 3072 Universal method to create market or limit orders with all available parameters for current `accountId`. 3073 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3074 3075 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3076 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3077 3078 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3079 then broker immediately open market order as you can do simple --buy or --sell operations! 3080 3081 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3082 When current price will go up or down to target price value then broker opens a limit order. 3083 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3084 3085 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3086 3087 :param operation: string "Buy" or "Sell". 3088 :param orderType: string "Limit" or "Stop". 3089 :param lots: volume, integer count of lots >= 1. 3090 :param targetPrice: target price > 0. This is open trade price for limit order. 3091 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3092 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3093 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3094 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3095 Stop loss order always executed by market price. 3096 :param expDate: string "Undefined" by default or local date in future. 3097 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3098 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3099 A limit order has no expiration date, it lasts until the end of the trading day. 3100 :return: JSON with response from broker server. 3101 """ 3102 if self.accountId is None or not self.accountId: 3103 uLogger.error("Variable `accountId` must be defined for using this method!") 3104 raise Exception("Account ID required") 3105 3106 if operation is None or not operation or operation not in ("Buy", "Sell"): 3107 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3108 raise Exception("Incorrect value") 3109 3110 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3111 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3112 raise Exception("Incorrect value") 3113 3114 if lots is None or lots < 1: 3115 uLogger.error("You must define trade volume > 0: integer count of lots!") 3116 raise Exception("Incorrect value") 3117 3118 if targetPrice is None or targetPrice <= 0: 3119 uLogger.error("Target price for limit-order must be greater than 0!") 3120 raise Exception("Incorrect value") 3121 3122 if limitPrice is None or limitPrice <= 0: 3123 limitPrice = targetPrice 3124 3125 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3126 stopType = "Limit" 3127 3128 if expDate is None or not expDate: 3129 expDate = "Undefined" 3130 3131 if not (self.ticker or self.figi): 3132 uLogger.error("Tocker or FIGI must be defined!") 3133 raise Exception("Ticker or FIGI required") 3134 3135 response = {} 3136 instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False) 3137 self.ticker = instrument["ticker"] 3138 self.figi = instrument["figi"] 3139 3140 if orderType == "Limit": 3141 uLogger.debug( 3142 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3143 self.ticker, self.figi, 3144 operation, lots, targetPrice, instrument["currency"], 3145 )) 3146 3147 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3148 self.body = str({ 3149 "figi": self.figi, 3150 "quantity": str(lots), 3151 "price": FloatToNano(targetPrice), 3152 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3153 "accountId": str(self.accountId), 3154 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3155 }) 3156 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False) 3157 3158 if "orderId" in response.keys(): 3159 uLogger.info( 3160 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3161 response["orderId"], 3162 self.ticker, self.figi, 3163 operation, lots, targetPrice, instrument["currency"], 3164 )) 3165 3166 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3167 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3168 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3169 targetPrice, instrument["currency"], 3170 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3171 )) 3172 3173 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3174 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3175 targetPrice, instrument["currency"], 3176 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3177 )) 3178 3179 else: 3180 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.") 3181 3182 if orderType == "Stop": 3183 uLogger.debug( 3184 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3185 self.ticker, self.figi, 3186 operation, lots, 3187 targetPrice, instrument["currency"], 3188 limitPrice, instrument["currency"], 3189 stopType, expDate, 3190 )) 3191 3192 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3193 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3194 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3195 3196 body = { 3197 "figi": self.figi, 3198 "quantity": str(lots), 3199 "price": FloatToNano(limitPrice), 3200 "stopPrice": FloatToNano(targetPrice), 3201 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3202 "accountId": str(self.accountId), 3203 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3204 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3205 } 3206 3207 if expDateUTC: 3208 body["expireDate"] = expDateUTC 3209 3210 self.body = str(body) 3211 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False) 3212 3213 if "stopOrderId" in response.keys(): 3214 uLogger.info( 3215 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3216 response["stopOrderId"], 3217 self.ticker, self.figi, 3218 operation, lots, 3219 targetPrice, instrument["currency"], 3220 limitPrice, instrument["currency"], 3221 TKS_STOP_ORDER_TYPES[stopOrderType], 3222 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3223 )) 3224 3225 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3226 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3227 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3228 targetPrice, instrument["currency"], 3229 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3230 )) 3231 3232 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3233 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3234 targetPrice, instrument["currency"], 3235 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3236 )) 3237 3238 else: 3239 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.") 3240 3241 return response 3242 3243 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3244 """ 3245 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3246 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3247 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3248 See also: `Order()` docstring. 3249 3250 :param lots: volume, integer count of lots >= 1. 3251 :param targetPrice: target price > 0. This is open trade price for limit order. 3252 :return: JSON with response from broker server. 3253 """ 3254 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3255 3256 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3257 """ 3258 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3259 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3260 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3261 target price value then broker opens a limit order. See also: `Order()` docstring. 3262 3263 :param lots: volume, integer count of lots >= 1. 3264 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3265 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3266 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3267 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3268 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3269 :param expDate: string "Undefined" by default or local date in future. 3270 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3271 This date is converting to UTC format for server. 3272 :return: JSON with response from broker server. 3273 """ 3274 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3275 3276 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3277 """ 3278 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3279 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3280 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3281 See also: `Order()` docstring. 3282 3283 :param lots: volume, integer count of lots >= 1. 3284 :param targetPrice: target price > 0. This is open trade price for limit order. 3285 :return: JSON with response from broker server. 3286 """ 3287 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3288 3289 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3290 """ 3291 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3292 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3293 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3294 target price value then broker opens a limit order. See also: `Order()` docstring. 3295 3296 :param lots: volume, integer count of lots >= 1. 3297 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3298 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3299 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3300 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3301 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3302 :param expDate: string "Undefined" by default or local date in future. 3303 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3304 This date is converting to UTC format for server. 3305 :return: JSON with response from broker server. 3306 """ 3307 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3308 3309 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3310 """ 3311 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3312 3313 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3314 :param allOrdersIDs: pre-received lists of all active pending orders. 3315 This avoids unnecessary downloading data from the server. 3316 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3317 """ 3318 if self.accountId is None or not self.accountId: 3319 uLogger.error("Variable `accountId` must be defined for using this method!") 3320 raise Exception("Account ID required") 3321 3322 if orderIDs: 3323 if allOrdersIDs is None or not allOrdersIDs: 3324 rawOrders = self.RequestPendingOrders() 3325 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3326 3327 if allStopOrdersIDs is None or not allStopOrdersIDs: 3328 rawStopOrders = self.RequestStopOrders() 3329 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3330 3331 for orderID in orderIDs: 3332 idInPendingOrders = orderID in allOrdersIDs 3333 idInStopOrders = orderID in allStopOrdersIDs 3334 3335 if not (idInPendingOrders or idInStopOrders): 3336 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3337 continue 3338 3339 else: 3340 if idInPendingOrders: 3341 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3342 3343 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3344 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3345 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3346 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3347 3348 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3349 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3350 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3351 3352 else: 3353 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3354 3355 elif idInStopOrders: 3356 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3357 3358 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3359 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3360 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3361 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3362 3363 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3364 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3365 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3366 3367 else: 3368 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3369 3370 else: 3371 continue 3372 3373 def CloseAllOrders(self) -> None: 3374 """ 3375 Gets a list of open pending and stop orders and cancel it all. 3376 """ 3377 rawOrders = self.RequestPendingOrders() 3378 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3379 lenOrders = len(allOrdersIDs) 3380 3381 rawStopOrders = self.RequestStopOrders() 3382 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3383 lenSOrders = len(allStopOrdersIDs) 3384 3385 if lenOrders > 0 or lenSOrders > 0: 3386 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3387 3388 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3389 3390 else: 3391 uLogger.info("Orders not found, nothing to cancel.") 3392 3393 def CloseAll(self, *args) -> None: 3394 """ 3395 Close all available (not blocked) opened trades and orders. 3396 3397 Also, you can select one or more keywords case-insensitive: 3398 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3399 3400 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3401 """ 3402 overview = self.Overview(show=False) # get all open trades info 3403 3404 if len(args) == 0: 3405 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3406 self.CloseAllOrders() # close all pending and stop orders 3407 3408 for iType in TKS_INSTRUMENTS: 3409 if iType != "Currencies": 3410 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3411 3412 else: 3413 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3414 lowerArgs = [x.lower() for x in args] 3415 3416 if "orders" in lowerArgs: 3417 self.CloseAllOrders() # close all pending and stop orders 3418 3419 for iType in TKS_INSTRUMENTS: 3420 if iType.lower() in lowerArgs and iType != "Currencies": 3421 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3422 3423 @staticmethod 3424 def ParseOrderParameters(operation, **inputParameters): 3425 """ 3426 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3427 3428 :param operation: string "Buy" or "Sell". 3429 :param inputParameters: this is dict of strings that looks like this 3430 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3431 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3432 "prices" key: one or more prices to open limit-orders 3433 Counts of values in lots and prices lists must be equals! 3434 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3435 """ 3436 # TODO: update order grid work with api v2 3437 pass 3438 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3439 # 3440 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3441 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3442 # raise Exception("Incorrect value") 3443 # 3444 # if "l" in inputParameters.keys(): 3445 # inputParameters["lots"] = inputParameters.pop("l") 3446 # 3447 # if "p" in inputParameters.keys(): 3448 # inputParameters["prices"] = inputParameters.pop("p") 3449 # 3450 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3451 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3452 # raise Exception("Incorrect value") 3453 # 3454 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3455 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3456 # 3457 # if len(lots) != len(prices): 3458 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3459 # raise Exception("Incorrect value") 3460 # 3461 # uLogger.debug("Extracted parameters for orders:") 3462 # uLogger.debug("lots = {}".format(lots)) 3463 # uLogger.debug("prices = {}".format(prices)) 3464 # 3465 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3466 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3467 # uLogger.debug("Order parameters: {}".format(result)) 3468 # 3469 # return result 3470 3471 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3472 """ 3473 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3474 3475 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3476 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3477 """ 3478 result = False 3479 msg = "Instrument not defined!" 3480 3481 if portfolio is None or not portfolio: 3482 portfolio = self.Overview(show=False) 3483 3484 if self.ticker: 3485 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3486 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3487 3488 for iType in TKS_INSTRUMENTS: 3489 for instrument in portfolio["stat"][iType]: 3490 if instrument["ticker"] == self.ticker: 3491 result = True 3492 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3493 break 3494 3495 elif self.figi: 3496 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3497 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3498 3499 for iType in TKS_INSTRUMENTS: 3500 for instrument in portfolio["stat"][iType]: 3501 if instrument["figi"] == self.figi: 3502 result = True 3503 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3504 break 3505 3506 else: 3507 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3508 3509 uLogger.debug(msg) 3510 3511 return result 3512 3513 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3514 """ 3515 Returns instrument is in the user's portfolio if it presents there. 3516 Instrument must be defined by `ticker` (highly priority) or `figi`. 3517 3518 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3519 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3520 """ 3521 result = None 3522 msg = "Instrument not defined!" 3523 3524 if portfolio is None or not portfolio: 3525 portfolio = self.Overview(show=False) 3526 3527 if self.ticker: 3528 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3529 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3530 3531 for iType in TKS_INSTRUMENTS: 3532 for instrument in portfolio["stat"][iType]: 3533 if instrument["ticker"] == self.ticker: 3534 result = instrument 3535 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3536 break 3537 3538 elif self.figi: 3539 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3540 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3541 3542 for iType in TKS_INSTRUMENTS: 3543 for instrument in portfolio["stat"][iType]: 3544 if instrument["figi"] == self.figi: 3545 result = instrument 3546 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3547 break 3548 3549 else: 3550 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3551 3552 uLogger.debug(msg) 3553 3554 return result 3555 3556 def RequestLimits(self) -> dict: 3557 """ 3558 Method for obtaining the available funds for withdrawal for current `accountId`. 3559 3560 See also: 3561 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3562 - `OverviewLimits()` method 3563 3564 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3565 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3566 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3567 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3568 """ 3569 if self.accountId is None or not self.accountId: 3570 uLogger.error("Variable `accountId` must be defined for using this method!") 3571 raise Exception("Account ID required") 3572 3573 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3574 3575 self.body = str({"accountId": self.accountId}) 3576 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3577 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3578 3579 uLogger.debug("Records about available funds for withdrawal successfully received") 3580 3581 return rawLimits 3582 3583 def OverviewLimits(self, show: bool = False) -> dict: 3584 """ 3585 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3586 3587 See also: `RequestLimits()`. 3588 3589 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3590 :return: dict with raw parsed data from server and some calculated statistics about it. 3591 """ 3592 if self.accountId is None or not self.accountId: 3593 uLogger.error("Variable `accountId` must be defined for using this method!") 3594 raise Exception("Account ID required") 3595 3596 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3597 3598 view = { 3599 "rawLimits": rawLimits, 3600 "limits": { # parsed data for every currency: 3601 "money": { # this is an array of portfolio currency positions 3602 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3603 }, 3604 "blocked": { # this is an array of blocked currency 3605 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3606 }, 3607 "blockedGuarantee": { # this is locked money under collateral for futures 3608 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3609 }, 3610 }, 3611 } 3612 3613 # --- Prepare text table with limits in human-readable format: 3614 if show: 3615 info = [ 3616 "# Withdrawal limits\n\n", 3617 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3618 "* **Account ID:** [{}]\n".format(self.accountId), 3619 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3620 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3621 ] 3622 3623 for curr in view["limits"]["money"].keys(): 3624 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3625 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3626 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3627 3628 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3629 "[{}]".format(curr), 3630 "{:.2f}".format(view["limits"]["money"][curr]), 3631 "{:.2f}".format(availableMoney), 3632 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3633 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3634 ) 3635 3636 if curr == "rub": 3637 info.insert(5, infoStr) # insert at first position in table and after headers 3638 3639 else: 3640 info.append(infoStr) 3641 3642 infoText = "".join(info) 3643 3644 uLogger.info(infoText) 3645 3646 if self.withdrawalLimitsFile: 3647 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3648 fH.write(infoText) 3649 3650 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3651 3652 return view 3653 3654 def RequestAccounts(self) -> dict: 3655 """ 3656 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3657 3658 See also: 3659 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3660 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3661 - `OverviewUserInfo()` method 3662 3663 :return: dict with raw data from server that contains accounts info. Example of dict: 3664 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3665 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3666 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3667 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3668 """ 3669 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3670 3671 self.body = str({}) 3672 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3673 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3674 3675 uLogger.debug("Records about available accounts successfully received") 3676 3677 return rawAccounts 3678 3679 def RequestUserInfo(self) -> dict: 3680 """ 3681 Method for requesting common user's information. 3682 3683 See also: 3684 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3685 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3686 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3687 - `OverviewUserInfo()` method 3688 3689 :return: dict with raw data from server that contains user's information. Example of dict: 3690 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3691 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3692 """ 3693 uLogger.debug("Requesting common user's information. Wait, please...") 3694 3695 self.body = str({}) 3696 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3697 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3698 3699 uLogger.debug("Records about current user successfully received") 3700 3701 return rawUserInfo 3702 3703 def RequestMarginStatus(self, accountId: str = None) -> dict: 3704 """ 3705 Method for requesting margin calculation for defined account ID. 3706 3707 See also: 3708 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3709 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3710 - `OverviewUserInfo()` method 3711 3712 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3713 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3714 Example of responses: 3715 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3716 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3717 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3718 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3719 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3720 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3721 """ 3722 if accountId is None or not accountId: 3723 if self.accountId is None or not self.accountId: 3724 uLogger.error("Variable `accountId` must be defined for using this method!") 3725 raise Exception("Account ID required") 3726 3727 else: 3728 accountId = self.accountId # use `self.accountId` (main ID) by default 3729 3730 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3731 3732 self.body = str({"accountId": accountId}) 3733 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3734 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3735 3736 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3737 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3738 rawMargin = {} 3739 3740 else: 3741 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3742 3743 return rawMargin 3744 3745 def RequestTariffLimits(self) -> dict: 3746 """ 3747 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3748 3749 See also: 3750 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3751 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3752 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3753 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3754 - `OverviewUserInfo()` method 3755 3756 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3757 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3758 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3759 """ 3760 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3761 3762 self.body = str({}) 3763 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3764 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3765 3766 uLogger.debug("Records with limits of current tariff successfully received") 3767 3768 return rawTariffLimits 3769 3770 def RequestBondCoupons(self, iJSON: dict) -> dict: 3771 """ 3772 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3773 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 3774 All dates are in UTC timezone. 3775 3776 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3777 Documentation: 3778 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3779 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3780 3781 See also: `ExtendBondsData()`. 3782 3783 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]` 3784 If raw iJSON is not data of bond then server returns an error [400] with message: 3785 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3786 :return: dictionary with bond payment calendar. Response example 3787 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3788 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3789 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3790 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3791 """ 3792 if iJSON["figi"] is None or not iJSON["figi"]: 3793 uLogger.error("FIGI must be defined for using this method!") 3794 raise Exception("FIGI required") 3795 3796 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 3797 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 3798 3799 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 3800 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 3801 self.figi, 3802 startDate, 3803 endDate, 3804 )) 3805 3806 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 3807 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 3808 calendar = self.SendAPIRequest(calendarURL, reqType="POST", debug=False) 3809 3810 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 3811 uLogger.warning("Instrument type is not bond!") 3812 3813 else: 3814 uLogger.debug("Records about bond payment calendar successfully received") 3815 3816 return calendar 3817 3818 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 3819 """ 3820 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 3821 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 3822 coupon yields, current yields and some statistics etc. 3823 3824 WARNING! This is too long operation if a lot of bonds requested from broker server. 3825 3826 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 3827 3828 :param instruments: list of strings with tickers or FIGIs. 3829 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 3830 for further used by data scientists or stock analytics. 3831 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 3832 In XLSX-file and Pandas DataFrame fields mean: 3833 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 3834 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 3835 """ 3836 if instruments is None or not instruments: 3837 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3838 raise Exception("Ticker or FIGI required") 3839 3840 if isinstance(instruments, str): 3841 instruments = [instruments] 3842 3843 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3844 3845 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 3846 3847 iCount = len(uniqueInstruments) 3848 tooLong = iCount >= 20 3849 if tooLong: 3850 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 3851 3852 bonds = None 3853 for i, self.figi in enumerate(uniqueInstruments): 3854 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 3855 3856 if "type" in instrument.keys() and instrument["type"] == "Bonds": 3857 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3858 rawBond = self.SearchByFIGI(requestPrice=True) 3859 3860 # Widen raw data with UTC current time (iData["actualDateTime"]): 3861 actualDate = datetime.now(tzutc()) 3862 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 3863 3864 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 3865 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 3866 3867 # Replace some values with human-readable: 3868 iData["nominalCurrency"] = iData["nominal"]["currency"] 3869 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 3870 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 3871 iData["aciCurrency"] = iData["aciValue"]["currency"] 3872 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 3873 iData["issueSize"] = int(iData["issueSize"]) 3874 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 3875 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 3876 iData["step"] = iData["step"] if "step" in iData.keys() else 0 3877 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 3878 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 3879 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 3880 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 3881 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 3882 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 3883 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 3884 3885 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 3886 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 3887 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 3888 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 3889 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 3890 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 3891 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 3892 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 3893 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 3894 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 3895 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 3896 3897 # Widen raw data with calendar data from `rawCalendar` values: 3898 calendarData = [] 3899 for item in iData["rawCalendar"]["events"]: 3900 calendarData.append({ 3901 "couponDate": item["couponDate"], 3902 "couponNumber": int(item["couponNumber"]), 3903 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 3904 "payCurrency": item["payOneBond"]["currency"], 3905 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 3906 "couponType": TKS_COUPON_TYPES[item["couponType"]], 3907 "couponStartDate": item["couponStartDate"], 3908 "couponEndDate": item["couponEndDate"], 3909 "couponPeriod": item["couponPeriod"], 3910 }) 3911 3912 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 3913 if "maturityDate" not in iData.keys(): 3914 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 3915 3916 # Widen raw data with Coupon Rate. 3917 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 3918 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 3919 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 3920 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 3921 3922 # Widen raw data with Yield to Maturity (YTM) on current date. 3923 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 3924 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 3925 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 3926 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 3927 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 3928 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 3929 3930 iData["calendar"] = calendarData # adds calendar at the end 3931 3932 # Remove not used data: 3933 iData.pop("uid") 3934 iData.pop("positionUid") 3935 iData.pop("currentPrice") 3936 iData.pop("rawCalendar") 3937 3938 colNames = list(iData.keys()) 3939 if bonds is None: 3940 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 3941 3942 else: 3943 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 3944 3945 else: 3946 uLogger.warning("Instrument with ticker [{}] and FIGI [{}] is not a bond!".format(instrument["ticker"], instrument["figi"])) 3947 3948 processed = round(100 * (i + 1) / iCount, 1) 3949 if tooLong and processed % 5 == 0: 3950 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 3951 3952 else: 3953 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 3954 3955 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 3956 3957 # Saving bonds from Pandas DataFrame to XLSX sheet: 3958 if xlsx and self.bondsXLSXFile: 3959 with pd.ExcelWriter( 3960 path=self.bondsXLSXFile, 3961 date_format=TKS_DATE_FORMAT, 3962 datetime_format=TKS_DATE_TIME_FORMAT, 3963 mode="w", 3964 ) as writer: 3965 bonds.to_excel( 3966 writer, 3967 sheet_name="Extended bonds data", 3968 index=True, 3969 encoding="UTF-8", 3970 freeze_panes=(1, 1), 3971 ) # saving as XLSX-file with freeze first row and column as headers 3972 3973 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 3974 3975 return bonds 3976 3977 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 3978 """ 3979 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 3980 3981 WARNING! This is too long operation if a lot of bonds requested from broker server. 3982 3983 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 3984 3985 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 3986 extended information about bonds: main info, current prices, bond payment calendar, 3987 coupon yields, current yields and some statistics etc. 3988 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 3989 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 3990 for further used by data scientists or stock analytics. 3991 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 3992 """ 3993 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 3994 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 3995 3996 uLogger.debug("Generating bond payments calendar data. Wait, please...") 3997 3998 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 3999 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4000 calendar = None 4001 for bond in extBonds.iterrows(): 4002 for item in bond[1]["calendar"]: 4003 cData = { 4004 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4005 "couponDate": item["couponDate"], 4006 "figi": bond[1]["figi"], 4007 "ticker": bond[1]["ticker"], 4008 "name": bond[1]["name"], 4009 "couponNumber": item["couponNumber"], 4010 "payOneBond": item["payOneBond"], 4011 "payCurrency": item["payCurrency"], 4012 "couponType": item["couponType"], 4013 "couponPeriod": item["couponPeriod"], 4014 "fixDate": item["fixDate"], 4015 "couponStartDate": item["couponStartDate"], 4016 "couponEndDate": item["couponEndDate"], 4017 } 4018 4019 if calendar is None: 4020 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4021 4022 else: 4023 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4024 4025 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4026 4027 # Saving calendar from Pandas DataFrame to XLSX sheet: 4028 if xlsx: 4029 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4030 4031 with pd.ExcelWriter( 4032 path=xlsxCalendarFile, 4033 date_format=TKS_DATE_FORMAT, 4034 datetime_format=TKS_DATE_TIME_FORMAT, 4035 mode="w", 4036 ) as writer: 4037 humanReadable = calendar.copy(deep=True) 4038 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4039 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4040 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4041 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4042 humanReadable.columns = colNames # human-readable column names 4043 4044 humanReadable.to_excel( 4045 writer, 4046 sheet_name="Bond payments calendar", 4047 index=False, 4048 encoding="UTF-8", 4049 freeze_panes=(1, 2), 4050 ) # saving as XLSX-file with freeze first row and column as headers 4051 4052 del humanReadable # release df in memory 4053 4054 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4055 4056 return calendar 4057 4058 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4059 """ 4060 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4061 Also, creates Markdown file with calendar data, `calendar.md` by default. 4062 4063 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4064 4065 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4066 extended information about bonds: main info, current prices, bond payment calendar, 4067 coupon yields, current yields and some statistics etc. 4068 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4069 :param show: if `True` then also printing bonds payment calendar to the console, 4070 otherwise save to file `calendarFile` only. `False` by default. 4071 :return: multilines text in Markdown format with bonds payment calendar as a table. 4072 """ 4073 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4074 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4075 4076 infoText = "# Bond payments calendar\n\n" 4077 4078 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4079 4080 if not calendar.empty: 4081 splitLine = "| | | | | | | | | |\n" 4082 4083 info = [ 4084 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4085 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4086 ] 4087 4088 newMonth = False 4089 notOneBond = calendar["figi"].nunique() > 1 4090 for i, bond in enumerate(calendar.iterrows()): 4091 if newMonth and notOneBond: 4092 info.append(splitLine) 4093 4094 info.append( 4095 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4096 " √" if bond[1]["paid"] else " —", 4097 bond[1]["couponDate"].split("T")[0], 4098 bond[1]["figi"], 4099 bond[1]["ticker"], 4100 bond[1]["couponNumber"], 4101 "{} {}".format( 4102 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4103 bond[1]["payCurrency"], 4104 ), 4105 bond[1]["couponType"], 4106 bond[1]["couponPeriod"], 4107 bond[1]["fixDate"].split("T")[0], 4108 ) 4109 ) 4110 4111 if i < len(calendar.values) - 1: 4112 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4113 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4114 newMonth = False if curDate.month == nextDate.month else True 4115 4116 else: 4117 newMonth = False 4118 4119 infoText += "".join(info) 4120 4121 if show: 4122 uLogger.info("{}".format(infoText)) 4123 4124 if self.calendarFile is not None: 4125 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4126 fH.write(infoText) 4127 4128 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4129 4130 else: 4131 infoText += "No data\n" 4132 4133 return infoText 4134 4135 def OverviewAccounts(self, show: bool = False) -> dict: 4136 """ 4137 Method for parsing and show simple table with all available user accounts. 4138 4139 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4140 4141 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4142 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4143 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4144 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4145 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4146 "closed": "—", "access": "Full access" }, ...}}` 4147 """ 4148 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4149 4150 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4151 accounts = { 4152 item["id"]: { 4153 "type": TKS_ACCOUNT_TYPES[item["type"]], 4154 "name": item["name"], 4155 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4156 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4157 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4158 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4159 } for item in rawAccounts["accounts"] 4160 } 4161 4162 # Raw and parsed data with some fields replaced in "stat" section: 4163 view = { 4164 "rawAccounts": rawAccounts, 4165 "stat": accounts, 4166 } 4167 4168 # --- Prepare simple text table with only accounts data in human-readable format: 4169 if show: 4170 info = [ 4171 "# User accounts\n\n", 4172 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4173 "| Account ID | Type | Status | Name |\n", 4174 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4175 ] 4176 4177 for account in view["stat"].keys(): 4178 info.extend([ 4179 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4180 account, 4181 view["stat"][account]["type"], 4182 view["stat"][account]["status"], 4183 view["stat"][account]["name"], 4184 ) 4185 ]) 4186 4187 infoText = "".join(info) 4188 4189 uLogger.info(infoText) 4190 4191 if self.userAccountsFile: 4192 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4193 fH.write(infoText) 4194 4195 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4196 4197 return view 4198 4199 def OverviewUserInfo(self, show: bool = False) -> dict: 4200 """ 4201 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4202 4203 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4204 4205 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4206 :return: dict with raw parsed data from server and some calculated statistics about it. 4207 """ 4208 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4209 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4210 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4211 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4212 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4213 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4214 4215 # This is dict with parsed common user data: 4216 userInfo = { 4217 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4218 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4219 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4220 "tariff": rawUserInfo["tariff"], 4221 } 4222 4223 # This is an array of dict with parsed margin statuses for every account IDs: 4224 margins = {} 4225 for accountId in accounts.keys(): 4226 if rawMargins[accountId]: 4227 margins[accountId] = { 4228 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4229 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4230 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4231 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4232 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4233 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4234 } 4235 4236 else: 4237 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4238 4239 unary = {} # unary-connection limits 4240 for item in rawTariffLimits["unaryLimits"]: 4241 if item["limitPerMinute"] in unary.keys(): 4242 unary[item["limitPerMinute"]].extend(item["methods"]) 4243 4244 else: 4245 unary[item["limitPerMinute"]] = item["methods"] 4246 4247 stream = {} # stream-connection limits 4248 for item in rawTariffLimits["streamLimits"]: 4249 if item["limit"] in stream.keys(): 4250 stream[item["limit"]].extend(item["streams"]) 4251 4252 else: 4253 stream[item["limit"]] = item["streams"] 4254 4255 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4256 limits = { 4257 "unary": unary, 4258 "stream": stream, 4259 } 4260 4261 # Raw and parsed data as an output result: 4262 view = { 4263 "rawUserInfo": rawUserInfo, 4264 "rawAccounts": rawAccounts, 4265 "rawMargins": rawMargins, 4266 "rawTariffLimits": rawTariffLimits, 4267 "stat": { 4268 "userInfo": userInfo, 4269 "accounts": accounts, 4270 "margins": margins, 4271 "limits": limits, 4272 }, 4273 } 4274 4275 # --- Prepare text table with user information in human-readable format: 4276 if show: 4277 info = [ 4278 "# Full user information\n\n", 4279 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4280 "## Common information\n\n", 4281 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4282 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4283 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4284 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4285 "\n## User accounts\n\n", 4286 ] 4287 4288 for account in view["stat"]["accounts"].keys(): 4289 info.extend([ 4290 "### ID: [{}]\n\n".format(account), 4291 "| Parameters | Values |\n", 4292 "|----------------------|--------------------------------------------------------------|\n", 4293 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4294 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4295 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4296 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4297 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4298 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4299 ]) 4300 4301 if margins[account]: 4302 info.extend([ 4303 "| Margin status: | Enabled |\n", 4304 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4305 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4306 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4307 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4308 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4309 ]) 4310 4311 else: 4312 info.append("| Margin status: | Disabled |\n\n") 4313 4314 info.extend([ 4315 "\n## Current user tariff limits\n", 4316 "\nSee also:\n", 4317 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4318 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4319 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4320 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4321 "\n### Unary limits\n", 4322 ]) 4323 4324 if unary: 4325 for key, values in sorted(unary.items()): 4326 info.append("\n* Max requests per minute: {}\n".format(key)) 4327 4328 for value in values: 4329 info.append(" - {}\n".format(value)) 4330 4331 else: 4332 info.append("\nNot available\n") 4333 4334 info.append("\n### Stream limits\n") 4335 4336 if stream: 4337 for key, values in sorted(stream.items()): 4338 info.append("\n* Max stream connections: {}\n".format(key)) 4339 4340 for value in values: 4341 info.append(" - {}\n".format(value)) 4342 4343 else: 4344 info.append("\nNot available\n") 4345 4346 infoText = "".join(info) 4347 4348 uLogger.info(infoText) 4349 4350 if self.userInfoFile: 4351 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4352 fH.write(infoText) 4353 4354 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4355 4356 return view 4357 4358 4359class Args: 4360 """ 4361 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4362 """ 4363 def __init__(self, **kwargs): 4364 self.__dict__.update(kwargs) 4365 4366 def __getattr__(self, item): 4367 return None 4368 4369 4370def ParseArgs(): 4371 """This function get and parse command line keys.""" 4372 parser = ArgumentParser() # command-line string parser 4373 4374 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4375 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4376 4377 # --- options: 4378 4379 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4380 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4381 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4382 4383 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4384 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4385 4386 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4387 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4388 4389 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4390 4391 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4392 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4393 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4394 4395 parser.add_argument("--debug-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4396 4397 # --- commands: 4398 4399 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4400 4401 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4402 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4403 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4404 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4405 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4406 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4407 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4408 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4409 4410 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4411 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4412 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4413 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4414 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4415 4416 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4417 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4418 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4419 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4420 4421 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4422 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4423 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4424 4425 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4426 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4427 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4428 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4429 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4430 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4431 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4432 4433 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4434 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4435 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` key, including for currencies tickers.") 4436 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers, including for currencies tickers.") 4437 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.") 4438 4439 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4440 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4441 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4442 4443 cmdArgs = parser.parse_args() 4444 return cmdArgs 4445 4446 4447def Main(**kwargs): 4448 """ 4449 Main function for work with TKSBrokerAPI in the console. 4450 4451 See examples: 4452 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4453 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4454 """ 4455 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4456 4457 if args.debug_level: 4458 uLogger.level = 10 # always debug level by default 4459 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4460 4461 exitCode = 0 4462 start = datetime.now(tzutc()) 4463 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4464 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4465 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4466 )) 4467 4468 # trying to calculate full current version: 4469 buildVersion = __version__ 4470 try: 4471 v = version("tksbrokerapi") 4472 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4473 4474 except Exception: 4475 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4476 4477 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4478 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4479 4480 try: 4481 if args.version: 4482 print("TKSBrokerAPI {}".format(buildVersion)) 4483 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4484 4485 else: 4486 # Init class for trading with Tinkoff Broker: TODO: rename `server` to `trader` 4487 server = TinkoffBrokerServer( 4488 token=args.token, 4489 accountId=args.account_id, 4490 useCache=not args.no_cache, 4491 ) 4492 4493 # --- set some options: 4494 4495 if args.ticker: 4496 if args.ticker in server.aliasesKeys: 4497 server.ticker = server.aliases[args.ticker] # Replace some tickers with its aliases 4498 4499 else: 4500 server.ticker = args.ticker 4501 4502 if args.figi: 4503 server.figi = args.figi 4504 4505 if args.depth is not None: 4506 server.depth = args.depth 4507 4508 # --- do one of commands: 4509 4510 if args.list: 4511 if args.output is not None: 4512 server.instrumentsFile = args.output 4513 4514 server.ShowInstrumentsInfo(show=True) 4515 4516 elif args.list_xlsx: 4517 server.DumpInstrumentsAsXLSX(forceUpdate=False) 4518 4519 elif args.bonds_xlsx is not None: 4520 if args.output is not None: 4521 server.bondsXLSXFile = args.output 4522 4523 if len(args.bonds_xlsx) == 0: 4524 server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4525 4526 else: 4527 server.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4528 4529 elif args.search: 4530 if args.output is not None: 4531 server.searchResultsFile = args.output 4532 4533 server.SearchInstruments(pattern=args.search[0], show=True) 4534 4535 elif args.info: 4536 if not (args.ticker or args.figi): 4537 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4538 raise Exception("Ticker or FIGI required") 4539 4540 if args.output is not None: 4541 server.infoFile = args.output 4542 4543 if args.ticker: 4544 server.SearchByTicker(requestPrice=True, show=True, debug=False) # show info and current prices by ticker name 4545 4546 else: 4547 server.SearchByFIGI(requestPrice=True, show=True, debug=False) # show info and current prices by FIGI id 4548 4549 elif args.calendar is not None: 4550 if args.output is not None: 4551 server.calendarFile = args.output 4552 4553 if len(args.calendar) == 0: 4554 bondsData = server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4555 4556 else: 4557 bondsData = server.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4558 4559 server.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4560 4561 elif args.price: 4562 if not (args.ticker or args.figi): 4563 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4564 raise Exception("Ticker or FIGI required") 4565 4566 server.GetCurrentPrices(show=True) 4567 4568 elif args.prices is not None: 4569 if args.output is not None: 4570 server.pricesFile = args.output 4571 4572 server.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4573 4574 elif args.overview: 4575 if args.output is not None: 4576 server.overviewFile = args.output 4577 4578 server.Overview(show=True, details="full") 4579 4580 elif args.overview_digest: 4581 if args.output is not None: 4582 server.overviewDigestFile = args.output 4583 4584 server.Overview(show=True, details="digest") 4585 4586 elif args.overview_positions: 4587 if args.output is not None: 4588 server.overviewPositionsFile = args.output 4589 4590 server.Overview(show=True, details="positions") 4591 4592 elif args.overview_orders: 4593 if args.output is not None: 4594 server.overviewOrdersFile = args.output 4595 4596 server.Overview(show=True, details="orders") 4597 4598 elif args.overview_analytics: 4599 if args.output is not None: 4600 server.overviewAnalyticsFile = args.output 4601 4602 server.Overview(show=True, details="analytics") 4603 4604 elif args.deals is not None: 4605 if args.output is not None: 4606 server.reportFile = args.output 4607 4608 if 0 <= len(args.deals) < 3: 4609 server.Deals( 4610 start=args.deals[0] if len(args.deals) >= 1 else None, 4611 end=args.deals[1] if len(args.deals) == 2 else None, 4612 show=True, # Always show deals report in console 4613 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 4614 ) 4615 4616 else: 4617 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4618 raise Exception("Incorrect value") 4619 4620 elif args.history is not None: 4621 if args.output is not None: 4622 server.historyFile = args.output 4623 4624 if 0 <= len(args.history) < 3: 4625 dataReceived = server.History( 4626 start=args.history[0] if len(args.history) >= 1 else None, 4627 end=args.history[1] if len(args.history) == 2 else None, 4628 interval="hour" if args.interval is None or not args.interval else args.interval, 4629 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 4630 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 4631 show=True, # shows all downloaded candles in console 4632 ) 4633 4634 if args.render_chart is not None and dataReceived is not None: 4635 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4636 4637 server.ShowHistoryChart( 4638 candles=dataReceived, 4639 interact=iChart, 4640 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4641 ) 4642 4643 else: 4644 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4645 raise Exception("Incorrect value") 4646 4647 elif args.load_history is not None: 4648 histData = server.LoadHistory(filePath=args.load_history) # load data from file and show history in console 4649 4650 if args.render_chart is not None and histData is not None: 4651 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4652 server.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 4653 4654 server.ShowHistoryChart( 4655 candles=histData, 4656 interact=iChart, 4657 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4658 ) 4659 4660 elif args.trade is not None: 4661 if 1 <= len(args.trade) <= 5: 4662 server.Trade( 4663 operation=args.trade[0], 4664 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 4665 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 4666 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 4667 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 4668 ) 4669 4670 else: 4671 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4672 4673 elif args.buy is not None: 4674 if 0 <= len(args.buy) <= 4: 4675 server.Buy( 4676 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 4677 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 4678 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 4679 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 4680 ) 4681 4682 else: 4683 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4684 4685 elif args.sell is not None: 4686 if 0 <= len(args.sell) <= 4: 4687 server.Sell( 4688 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 4689 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 4690 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 4691 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 4692 ) 4693 4694 else: 4695 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4696 4697 elif args.order: 4698 if 4 <= len(args.order) <= 7: 4699 server.Order( 4700 operation=args.order[0], 4701 orderType=args.order[1], 4702 lots=int(args.order[2]), 4703 targetPrice=float(args.order[3]), 4704 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 4705 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 4706 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 4707 ) 4708 4709 else: 4710 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 4711 4712 elif args.buy_limit: 4713 server.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 4714 4715 elif args.sell_limit: 4716 server.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 4717 4718 elif args.buy_stop: 4719 if 2 <= len(args.buy_stop) <= 7: 4720 server.BuyStop( 4721 lots=int(args.buy_stop[0]), 4722 targetPrice=float(args.buy_stop[1]), 4723 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 4724 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 4725 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 4726 ) 4727 4728 else: 4729 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4730 4731 elif args.sell_stop: 4732 if 2 <= len(args.sell_stop) <= 7: 4733 server.SellStop( 4734 lots=int(args.sell_stop[0]), 4735 targetPrice=float(args.sell_stop[1]), 4736 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 4737 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 4738 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 4739 ) 4740 4741 else: 4742 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 4743 4744 # elif args.buy_order_grid is not None: 4745 # # update order grid work with api v2 4746 # if len(args.buy_order_grid) == 2: 4747 # orderParams = server.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 4748 # 4749 # for order in orderParams: 4750 # server.Order(operation="Buy", lots=order["lot"], price=order["price"]) 4751 # 4752 # else: 4753 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4754 # 4755 # elif args.sell_order_grid is not None: 4756 # # update order grid work with api v2 4757 # if len(args.sell_order_grid) >= 2: 4758 # orderParams = server.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 4759 # 4760 # for order in orderParams: 4761 # server.Order(operation="Sell", lots=order["lot"], price=order["price"]) 4762 # 4763 # else: 4764 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4765 4766 elif args.close_order is not None: 4767 server.CloseOrders(args.close_order) # close only one order 4768 4769 elif args.close_orders is not None: 4770 server.CloseOrders(args.close_orders) # close list of orders 4771 4772 elif args.close_trade: 4773 if not args.ticker: 4774 uLogger.error("`--ticker` key is required for this operation!") 4775 raise Exception("Ticker required") 4776 4777 server.CloseTrades([args.ticker]) # close only one trade 4778 4779 elif args.close_trades is not None: 4780 server.CloseTrades(args.close_trades) # close trades for list of tickers 4781 4782 elif args.close_all is not None: 4783 server.CloseAll(*args.close_all) 4784 4785 elif args.limits: 4786 if args.output is not None: 4787 server.withdrawalLimitsFile = args.output 4788 4789 server.OverviewLimits(show=True) 4790 4791 elif args.user_info: 4792 if args.output is not None: 4793 server.userInfoFile = args.output 4794 4795 server.OverviewUserInfo(show=True) 4796 4797 elif args.account: 4798 if args.output is not None: 4799 server.userAccountsFile = args.output 4800 4801 server.OverviewAccounts(show=True) 4802 4803 else: 4804 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 4805 raise Exception("There is no command to execute") 4806 4807 except Exception: 4808 trace = tb.format_exc() 4809 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 4810 if e in trace: 4811 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 4812 break 4813 4814 uLogger.debug(trace) 4815 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 4816 exitCode = 255 # an error occurred, must be open a ticket for this issue 4817 4818 finally: 4819 finish = datetime.now(tzutc()) 4820 4821 if exitCode == 0: 4822 uLogger.debug("All operations were finished success (summary code is 0).") 4823 4824 else: 4825 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 4826 os.path.abspath(uLog.defaultLogFile), exitCode, 4827 )) 4828 4829 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 4830 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 4831 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4832 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4833 )) 4834 4835 if not kwargs: 4836 sys.exit(exitCode) 4837 4838 else: 4839 return exitCode 4840 4841 4842if __name__ == "__main__": 4843 Main()
80def NanoToFloat(units: str, nano: int) -> float: 81 """ 82 Convert number in nano-view mode with string parameter `units` and integer parameter `nano` to float view. Examples: 83 84 `NanoToFloat(units="2", nano=500000000) -> 2.5` 85 86 `NanoToFloat(units="0", nano=50000000) -> 0.05` 87 88 :param units: integer string or integer parameter that represents the integer part of number 89 :param nano: integer string or integer parameter that represents the fractional part of number 90 :return: float view of number 91 """ 92 return int(units) + int(nano) * NANO
Convert number in nano-view mode with string parameter units and integer parameter nano to float view. Examples:
NanoToFloat(units="2", nano=500000000) -> 2.5
NanoToFloat(units="0", nano=50000000) -> 0.05
Parameters
- units: integer string or integer parameter that represents the integer part of number
- nano: integer string or integer parameter that represents the fractional part of number
Returns
float view of number
95def FloatToNano(number: float) -> dict: 96 """ 97 Convert float number to nano-type view: dictionary with string `units` and integer `nano` parameters `{"units": "string", "nano": integer}`. Examples: 98 99 `FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}` 100 101 `FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}` 102 103 :param number: float number 104 :return: nano-type view of number: `{"units": "string", "nano": integer}` 105 """ 106 splitByPoint = str(number).split(".") 107 frac = 0 108 109 if len(splitByPoint) > 1: 110 if len(splitByPoint[1]) <= 9: 111 frac = int("{}{}".format( 112 int(splitByPoint[1]), 113 "0" * (9 - len(splitByPoint[1])), 114 )) 115 116 if (number < 0) and (frac > 0): 117 frac = -frac 118 119 return {"units": str(int(number)), "nano": frac}
Convert float number to nano-type view: dictionary with string units and integer nano parameters {"units": "string", "nano": integer}. Examples:
FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}
FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}
Parameters
- number: float number
Returns
nano-type view of number:
{"units": "string", "nano": integer}
122def GetDatesAsString(start: str = None, end: str = None) -> tuple: 123 """ 124 Create tuple of date and time strings with timezone parsed from user-friendly date. 125 126 User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020). 127 128 Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") 129 An error exception will occur if input date has incorrect format. 130 131 If `start=None`, `end=None` then return dates from yesterday to the end of the day. 132 If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day. 133 If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`. 134 Start day may be negative integer numbers: `-1`, `-2`, `-3` - how many days ago. 135 136 Also, you can use keywords for start if `end=None`: 137 `today` (from 00:00:00 to the end of current day), 138 `yesterday` (-1 day from 00:00:00 to 23:59:59), 139 `week` (-7 day from 00:00:00 to the end of current day), 140 `month` (-30 day from 00:00:00 to the end of current day), 141 `year` (-365 day from 00:00:00 to the end of current day), 142 143 :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI. 144 See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`. 145 Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day. 146 """ 147 uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end)) 148 s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0) # start of the current day 149 e = s.replace(hour=23, minute=59, second=59, microsecond=0) # end of the current day 150 151 # time between start and the end of the current day: 152 if start is None or start.lower() == "today": 153 pass 154 155 # from start of the last day to the end of the last day: 156 elif start.lower() == "yesterday": 157 s -= timedelta(days=1) 158 e -= timedelta(days=1) 159 160 # week (-7 day from 00:00:00 to the end of the current day): 161 elif start.lower() == "week": 162 s -= timedelta(days=6) # +1 current day already taken into account 163 164 # month (-30 day from 00:00:00 to the end of current day): 165 elif start.lower() == "month": 166 s -= timedelta(days=29) # +1 current day already taken into account 167 168 # year (-365 day from 00:00:00 to the end of current day): 169 elif start.lower() == "year": 170 s -= timedelta(days=364) # +1 current day already taken into account 171 172 # -N days ago to the end of current day: 173 elif start.startswith('-') and start[1:].isdigit(): 174 s -= timedelta(days=abs(int(start)) - 1) # +1 current day already taken into account 175 176 # dates between start day at 00:00:00 and the end of the last day at 23:59:59: 177 else: 178 s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc()) 179 e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e 180 181 # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API: 182 s = s.strftime(TKS_DATE_TIME_FORMAT) 183 e = e.strftime(TKS_DATE_TIME_FORMAT) 184 185 uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e)) 186 187 return s, e
Create tuple of date and time strings with timezone parsed from user-friendly date.
User dates format must be like: %Y-%m-%d, e.g. 2020-02-03 (3 Feb, 2020).
Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") An error exception will occur if input date has incorrect format.
If start=None, end=None then return dates from yesterday to the end of the day.
If start=some_date_1, end=None then return dates from some_date_1 to the end of the day.
If start=some_date_1, end=some_date_2 then return dates from start of some_date_1 to end of some_date_2.
Start day may be negative integer numbers: -1, -2, -3 - how many days ago.
Also, you can use keywords for start if end=None:
today (from 00:00:00 to the end of current day),
yesterday (-1 day from 00:00:00 to 23:59:59),
week (-7 day from 00:00:00 to the end of current day),
month (-30 day from 00:00:00 to the end of current day),
year (-365 day from 00:00:00 to the end of current day),
Returns
tuple with 2 strings
(start, end)dates in UTC ISO time format%Y-%m-%dT%H:%M:%SZfor OpenAPI. See date and time format here:TKSEnums.TKS_DATE_TIME_FORMAT. Example:("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z"). Second string is the end of the last day.
190class TinkoffBrokerServer: 191 """ 192 This class implements methods to work with Tinkoff broker server. 193 194 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 195 196 About `token`: https://tinkoff.github.io/investAPI/token/ 197 """ 198 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 199 """ 200 Main class init. 201 202 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 203 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 204 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 205 :param useCache: use default cache file with raw data to use instead of `iList`. 206 True by default. Cache is auto-update if new day has come. 207 If you don't want to use cache and always updates raw data then set `useCache=False`. 208 :param defaultCache: path to default cache file. `dump.json` by default. 209 """ 210 if token is None or not token: 211 try: 212 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 213 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 214 215 except KeyError: 216 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 217 raise Exception("Token required") 218 219 else: 220 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 221 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 222 223 if accountId is None or not accountId: 224 try: 225 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 226 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 227 228 except KeyError: 229 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 230 231 else: 232 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 233 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 234 235 self.version = __version__ # duplicate here used TKSBrokerAPI main version 236 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 237 238 Latest version: https://pypi.org/project/tksbrokerapi/ 239 """ 240 241 self.aliases = TKS_TICKER_ALIASES 242 """Some aliases instead official tickers. 243 244 See also: `TKSEnums.TKS_TICKER_ALIASES` 245 """ 246 247 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 248 249 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 250 251 self.ticker = "" 252 """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 253 254 See also: `SearchByTicker()`, `SearchInstruments()`. 255 """ 256 257 self.figi = "" 258 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. 259 260 See also: `SearchByFIGI()`, `SearchInstruments()`. 261 """ 262 263 self.depth = 1 264 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 265 266 See also: `GetCurrentPrices()`. 267 """ 268 269 self.server = r"https://invest-public-api.tinkoff.ru/rest" 270 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 271 272 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 273 """ 274 275 uLogger.debug("Broker API server: {}".format(self.server)) 276 277 self.timeout = 15 278 """Server operations timeout in seconds. Default: `15`. 279 280 See also: `SendAPIRequest()`. 281 """ 282 283 self.headers = { 284 "Content-Type": "application/json", 285 "accept": "application/json", 286 "Authorization": "Bearer {}".format(self.token), 287 "x-app-name": "Tim55667757.TKSBrokerAPI", 288 } 289 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 290 291 See also: `SendAPIRequest()`. 292 """ 293 294 self.body = None 295 """Request body which send to broker server. Default: `None`. 296 297 See also: `SendAPIRequest()`. 298 """ 299 300 self.historyFile = None 301 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 302 303 See also: `History()`. 304 """ 305 306 self.htmlHistoryFile = "index.html" 307 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 308 309 See also: `ShowHistoryChart()`. 310 """ 311 312 self.instrumentsFile = "instruments.md" 313 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 314 315 See also: `ShowInstrumentsInfo()`. 316 """ 317 318 self.searchResultsFile = "search-results.md" 319 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 320 321 See also: `SearchInstruments()`. 322 """ 323 324 self.pricesFile = "prices.md" 325 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 326 327 See also: `GetListOfPrices()`. 328 """ 329 330 self.infoFile = "info.md" 331 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 332 333 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 334 """ 335 336 self.bondsXLSXFile = "ext-bonds.xlsx" 337 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 338 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 339 340 See also: `ExtendBondsData()`. 341 """ 342 343 self.calendarFile = "calendar.md" 344 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 345 346 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 347 348 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 349 """ 350 351 self.overviewFile = "overview.md" 352 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 353 354 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 355 """ 356 357 self.overviewDigestFile = "overview-digest.md" 358 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 359 360 See also: `Overview()` with parameter `details="digest"`. 361 """ 362 363 self.overviewPositionsFile = "overview-positions.md" 364 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 365 366 See also: `Overview()` with parameter `details="positions"`. 367 """ 368 369 self.overviewOrdersFile = "overview-orders.md" 370 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 371 372 See also: `Overview()` with parameter `details="orders"`. 373 """ 374 375 self.overviewAnalyticsFile = "overview-analytics.md" 376 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 377 378 See also: `Overview()` with parameter `details="analytics"`. 379 """ 380 381 self.reportFile = "deals.md" 382 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 383 384 See also: `Deals()`. 385 """ 386 387 self.withdrawalLimitsFile = "limits.md" 388 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 389 390 See also: `OverviewLimits()` and `RequestLimits()`. 391 """ 392 393 self.userInfoFile = "user-info.md" 394 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 395 396 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 397 """ 398 399 self.userAccountsFile = "accounts.md" 400 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 401 402 See also: `OverviewAccounts()`, `RequestAccounts()`. 403 """ 404 405 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 406 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 407 408 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 409 410 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 411 """ 412 413 self.iList = None # init iList for raw instruments data 414 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 415 416 See also: `Listing()`, `DumpInstruments()`. 417 """ 418 419 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 420 if useCache: 421 if os.path.exists(self.iListDumpFile): 422 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 423 curTime = datetime.now(tzutc()) 424 425 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 426 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 427 428 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 429 430 else: 431 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 432 433 uLogger.debug("Local cache with raw instruments data is used: [{}]".format(os.path.abspath(self.iListDumpFile))) 434 uLogger.debug("Dump file was last modified [{}] UTC".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 435 436 else: 437 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 438 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 439 440 else: 441 self.iList = self.Listing() # request new raw instruments data from broker server 442 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 443 444 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 445 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 446 447 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 448 """ 449 450 @staticmethod 451 def _ParseJSON(rawData="{}", debug: bool = False) -> dict: 452 """ 453 Parse JSON from response string. 454 455 :param rawData: this is a string with JSON-formatted text. 456 :param debug: if `True` then print more debug information. 457 :return: JSON (dictionary), parsed from server response string. 458 """ 459 if debug: 460 uLogger.debug("Raw text body:") 461 uLogger.debug(rawData) 462 463 responseJSON = json.loads(rawData) if rawData else {} 464 465 if debug: 466 uLogger.debug("JSON formatted:") 467 for jsonLine in json.dumps(responseJSON, indent=4).split('\n'): 468 uLogger.debug(jsonLine) 469 470 return responseJSON 471 472 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5, debug: bool = False) -> dict: 473 """ 474 Send GET or POST request to broker server and receive JSON object. 475 476 self.header: must be defining with dictionary of headers. 477 self.body: if define then used as request body. None by default. 478 self.timeout: global request timeout, 15 seconds by default. 479 :param url: url with REST request. 480 :param reqType: send "GET" or "POST" request. "GET" by default. 481 :param retry: how many times retry after first request if an 5xx server errors occurred. 482 :param pause: sleep time in seconds between retries. 483 :param debug: if `True` then print more debug information, e.g. request and response parameters, headers etc. 484 :return: response JSON (dictionary) from broker. 485 """ 486 if reqType not in ("GET", "POST"): 487 uLogger.error("You can define request type: 'GET' or 'POST'!") 488 raise Exception("Incorrect value") 489 490 if debug: 491 uLogger.debug("Request parameters:") 492 uLogger.debug(" - REST API URL: {}".format(url)) 493 uLogger.debug(" - request type: {}".format(reqType)) 494 uLogger.debug(" - headers: {}".format(str(self.headers).replace(self.token, "*** request token ***"))) 495 uLogger.debug(" - body: {}".format(self.body)) 496 497 # fast hack to avoid all operations with some tickers/FIGI 498 responseJSON = {} 499 oK = True 500 for item in self.exclude: 501 if item in url: 502 if debug: 503 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 504 505 oK = False 506 break 507 508 if oK: 509 counter = 0 510 response = None 511 errMsg = "" 512 513 while not response and counter <= retry: 514 if reqType == "GET": 515 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 516 517 if reqType == "POST": 518 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 519 520 if debug: 521 uLogger.debug("Response:") 522 uLogger.debug(" - status code: {}".format(response.status_code)) 523 uLogger.debug(" - reason: {}".format(response.reason)) 524 uLogger.debug(" - body length: {}".format(len(response.text))) 525 uLogger.debug(" - headers: {}".format(response.headers)) 526 527 # Server returns some headers: 528 # - `x-ratelimit-limit` - shows the settings of the current user limit for this method. 529 # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute. 530 # - `x-ratelimit-reset` - time in seconds before resetting the request counter. 531 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 532 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 533 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 534 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 535 sleep(rateLimitWait) 536 537 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 538 if 400 <= response.status_code < 500: 539 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 540 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 541 counter = retry + 1 542 543 if 500 <= response.status_code < 600: 544 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 545 uLogger.debug(" - not oK, {}".format(errMsg)) 546 counter += 1 547 548 if counter <= retry: 549 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 550 sleep(pause) 551 552 responseJSON = self._ParseJSON(response.text) 553 554 if errMsg: 555 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 556 uLogger.error(" - not oK, {}".format(errMsg)) 557 558 return responseJSON 559 560 def _IUpdater(self, iType: str) -> tuple: 561 """ 562 Request instrument by type from server. See available API methods for instruments: 563 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 564 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 565 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 566 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 567 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 568 569 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 570 :return: tuple with iType name and list of available instruments of current type for defined user token. 571 """ 572 result = [] 573 574 if iType in TKS_INSTRUMENTS: 575 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 576 577 # all instruments have the same body in API v2 requests: 578 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 579 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 580 result = self.SendAPIRequest(instrumentURL, reqType="POST", debug=False)["instruments"] 581 582 return iType, result 583 584 def _IWrapper(self, kwargs): 585 """ 586 Wrapper runs instrument's update method `_IUpdater()`. 587 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 588 """ 589 return self._IUpdater(**kwargs) 590 591 def Listing(self) -> dict: 592 """ 593 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 594 595 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 596 """ 597 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 598 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 599 600 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 601 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 602 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 603 604 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 605 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 606 poolUpdater.close() 607 608 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 609 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 610 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 611 612 # calculate minimum price increment (step) for all instruments and set up instrument's type: 613 for iType in iList.keys(): 614 for ticker in iList[iType]: 615 iList[iType][ticker]["type"] = iType 616 617 if "minPriceIncrement" in iList[iType][ticker].keys(): 618 iList[iType][ticker]["step"] = NanoToFloat( 619 iList[iType][ticker]["minPriceIncrement"]["units"], 620 iList[iType][ticker]["minPriceIncrement"]["nano"], 621 ) 622 623 else: 624 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 625 626 return iList 627 628 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 629 """ 630 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 631 632 See also: `DumpInstruments()`, `Listing()`. 633 634 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 635 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 636 """ 637 if self.iListDumpFile is None or not self.iListDumpFile: 638 uLogger.error("Output name of dump file must be defined!") 639 raise Exception("Filename required") 640 641 if not self.iList or forceUpdate: 642 self.iList = self.Listing() 643 644 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 645 646 # Save as XLSX with separated sheets for every type of instruments: 647 with pd.ExcelWriter( 648 path=xlsxDumpFile, 649 date_format=TKS_DATE_FORMAT, 650 datetime_format=TKS_DATE_TIME_FORMAT, 651 mode="w", 652 ) as writer: 653 for iType in TKS_INSTRUMENTS: 654 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 655 df = df[sorted(df)] # sorted by column names 656 df = df.applymap( 657 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 658 na_action="ignore", 659 ) # converting numbers from nano-type to float in every cell 660 df.to_excel( 661 writer, 662 sheet_name=iType, 663 encoding="UTF-8", 664 freeze_panes=(1, 1), 665 ) # saving as XLSX-file with freeze first row and column as headers 666 667 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 668 669 def DumpInstruments(self, forceUpdate: bool = True) -> str: 670 """ 671 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 672 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 673 674 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 675 676 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 677 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 678 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 679 """ 680 if self.iListDumpFile is None or not self.iListDumpFile: 681 uLogger.error("Output name of dump file must be defined!") 682 raise Exception("Filename required") 683 684 if not self.iList or forceUpdate: 685 self.iList = self.Listing() 686 687 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 688 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 689 fH.write(jsonDump) 690 691 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 692 693 return jsonDump 694 695 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 696 """ 697 Show information about one instrument defined by json data and prints it in Markdown format. 698 699 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 700 701 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 702 :param show: if `True` then also printing information about instrument and its current price. 703 :return: multilines text in Markdown format with information about one instrument. 704 """ 705 splitLine = "| | |\n" 706 infoText = "" 707 708 if iJSON is not None and iJSON and isinstance(iJSON, dict): 709 info = [ 710 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 711 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 712 "| Parameters | Values |\n", 713 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 714 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 715 "| Full name: | {:<54} |\n".format(iJSON["name"]), 716 ] 717 718 if "sector" in iJSON.keys() and iJSON["sector"]: 719 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 720 721 info.append("| Country of instrument: | {:<54} |\n".format("{}{}".format( 722 "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "", 723 iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "", 724 ))) 725 726 info.extend([ 727 splitLine, 728 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 729 "| Exchange: | {:<54} |\n".format(iJSON["exchange"]), 730 ]) 731 732 if "isin" in iJSON.keys() and iJSON["isin"]: 733 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 734 735 if "classCode" in iJSON.keys(): 736 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 737 738 info.extend([ 739 splitLine, 740 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 741 splitLine, 742 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 743 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 744 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 745 ]) 746 747 if iJSON["figi"]: 748 self.figi = iJSON["figi"] 749 iJSON = iJSON | self.RequestTradingStatus() 750 751 info.extend([ 752 splitLine, 753 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 754 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 755 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 756 ]) 757 758 info.append(splitLine) 759 760 if "type" in iJSON.keys() and iJSON["type"]: 761 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 762 763 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 764 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 765 766 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 767 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 768 769 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 770 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 771 772 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 773 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 774 775 if "focusType" in iJSON.keys() and iJSON["focusType"]: 776 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 777 778 if "assetType" in iJSON.keys() and iJSON["assetType"]: 779 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 780 781 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 782 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 783 784 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 785 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 786 787 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 788 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 789 790 if "currency" in iJSON.keys(): 791 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 792 793 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 794 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 795 796 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 797 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 798 799 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 800 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 801 802 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 803 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 804 805 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 806 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 807 808 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 809 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 810 811 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 812 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 813 814 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 815 info.append("| Perpetual bond: | Yes |\n") 816 817 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 818 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 819 820 iExt = None 821 if iJSON["type"] == "Bonds": 822 info.extend([ 823 splitLine, 824 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 825 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 826 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 827 iJSON["nominal"]["currency"], 828 )), 829 ]) 830 831 if "floatingCouponFlag" in iJSON.keys(): 832 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 833 834 if "amortizationFlag" in iJSON.keys(): 835 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 836 837 info.append(splitLine) 838 839 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 840 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 841 842 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 843 844 info.extend([ 845 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 846 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 847 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 848 ]) 849 850 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 851 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 852 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 853 iJSON["aciValue"]["currency"] 854 ))) 855 856 if "currentPrice" in iJSON.keys(): 857 info.append(splitLine) 858 859 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 860 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 861 862 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 863 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 864 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 865 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 866 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 867 868 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 869 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 870 871 info.extend([ 872 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 873 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 874 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 875 )), 876 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 877 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 878 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 879 )), 880 "| Changes between last deal price and last close | {:<54} |\n".format( 881 "{:.2f}%{}".format( 882 iJSON["currentPrice"]["changes"], 883 " ({}{:.2f} {})".format( 884 "+" if bondChangesDelta > 0 else "", 885 bondChangesDelta, 886 aciCurrency 887 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 888 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 889 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 890 currency 891 ), 892 ) 893 ), 894 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 895 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 896 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 897 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 898 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 899 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 900 )), 901 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 902 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 903 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 904 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 905 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 906 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 907 )), 908 ]) 909 910 if "lot" in iJSON.keys(): 911 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 912 913 if "step" in iJSON.keys() and iJSON["step"] != 0: 914 info.append("| Minimum price increment (step): | {:<54} |\n".format(iJSON["step"])) 915 916 # Add bond payment calendar: 917 if iJSON["type"] == "Bonds": 918 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 919 info.extend(["\n", strCalendar]) 920 921 infoText += "".join(info) 922 923 if show: 924 uLogger.info("{}".format(infoText)) 925 926 else: 927 uLogger.debug("{}".format(infoText)) 928 929 if self.infoFile is not None: 930 with open(self.infoFile, "w", encoding="UTF-8") as fH: 931 fH.write(infoText) 932 933 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 934 935 return infoText 936 937 def SearchByTicker(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict: 938 """ 939 Search and return raw broker's information about instrument by its ticker. 940 `ticker` must be defined! If debug=True then print all debug messages. 941 942 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 943 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 944 :param debug: if `True` then print all debug console messages. 945 :return: JSON formatted data with information about instrument. 946 """ 947 tickerJSON = {} 948 if debug: 949 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 950 951 if not self.ticker: 952 uLogger.warning("self.ticker variable is not be empty!") 953 954 else: 955 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 956 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 957 raise Exception("Instrument not allowed") 958 959 if not self.iList: 960 self.iList = self.Listing() 961 962 if self.ticker in self.iList["Shares"].keys(): 963 tickerJSON = self.iList["Shares"][self.ticker] 964 if debug: 965 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 966 967 elif self.ticker in self.iList["Currencies"].keys(): 968 tickerJSON = self.iList["Currencies"][self.ticker] 969 if debug: 970 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 971 972 elif self.ticker in self.iList["Bonds"].keys(): 973 tickerJSON = self.iList["Bonds"][self.ticker] 974 if debug: 975 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 976 977 elif self.ticker in self.iList["Etfs"].keys(): 978 tickerJSON = self.iList["Etfs"][self.ticker] 979 if debug: 980 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 981 982 elif self.ticker in self.iList["Futures"].keys(): 983 tickerJSON = self.iList["Futures"][self.ticker] 984 if debug: 985 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 986 987 if tickerJSON: 988 self.figi = tickerJSON["figi"] 989 990 if requestPrice: 991 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 992 993 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 994 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 995 996 else: 997 tickerJSON["currentPrice"]["changes"] = 0 998 999 if show: 1000 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 1001 1002 else: 1003 if show: 1004 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 1005 1006 return tickerJSON 1007 1008 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict: 1009 """ 1010 Search and return raw broker's information about instrument by its FIGI. 1011 `figi` must be defined! If debug=True then print all debug messages. 1012 1013 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1014 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1015 :param debug: if `True` then print all debug console messages. 1016 :return: JSON formatted data with information about instrument. 1017 """ 1018 figiJSON = {} 1019 if debug: 1020 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 1021 1022 if not self.figi: 1023 uLogger.warning("self.figi variable is not be empty!") 1024 1025 else: 1026 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1027 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 1028 raise Exception("Instrument not allowed") 1029 1030 if not self.iList: 1031 self.iList = self.Listing() 1032 1033 for item in self.iList["Shares"].keys(): 1034 if self.figi == self.iList["Shares"][item]["figi"]: 1035 figiJSON = self.iList["Shares"][item] 1036 1037 if debug: 1038 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 1039 1040 break 1041 1042 if not figiJSON: 1043 for item in self.iList["Currencies"].keys(): 1044 if self.figi == self.iList["Currencies"][item]["figi"]: 1045 figiJSON = self.iList["Currencies"][item] 1046 1047 if debug: 1048 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 1049 1050 break 1051 1052 if not figiJSON: 1053 for item in self.iList["Bonds"].keys(): 1054 if self.figi == self.iList["Bonds"][item]["figi"]: 1055 figiJSON = self.iList["Bonds"][item] 1056 1057 if debug: 1058 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 1059 1060 break 1061 1062 if not figiJSON: 1063 for item in self.iList["Etfs"].keys(): 1064 if self.figi == self.iList["Etfs"][item]["figi"]: 1065 figiJSON = self.iList["Etfs"][item] 1066 1067 if debug: 1068 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 1069 1070 break 1071 1072 if not figiJSON: 1073 for item in self.iList["Futures"].keys(): 1074 if self.figi == self.iList["Futures"][item]["figi"]: 1075 figiJSON = self.iList["Futures"][item] 1076 1077 if debug: 1078 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 1079 1080 break 1081 1082 if figiJSON: 1083 self.figi = figiJSON["figi"] 1084 self.ticker = figiJSON["ticker"] 1085 1086 if requestPrice: 1087 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1088 1089 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1090 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1091 1092 else: 1093 figiJSON["currentPrice"]["changes"] = 0 1094 1095 if show: 1096 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1097 1098 else: 1099 if show: 1100 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 1101 1102 return figiJSON 1103 1104 def GetCurrentPrices(self, show: bool = True) -> dict: 1105 """ 1106 Get and show Depth of Market with current prices of the instrument. If an error occurred then returns an empty record: 1107 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1108 1109 See also: 1110 1111 :param show: if `True` then print DOM to log and console. 1112 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1113 """ 1114 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1115 1116 if self.depth < 1: 1117 uLogger.error("Depth of Market (DOM) must be >=1!") 1118 raise Exception("Incorrect value") 1119 1120 if not (self.ticker or self.figi): 1121 uLogger.error("self.ticker or self.figi variables must be defined!") 1122 raise Exception("Ticker or FIGI required") 1123 1124 if self.ticker and not self.figi: 1125 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1126 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1127 1128 if not self.ticker and self.figi: 1129 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1130 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1131 1132 if not self.figi: 1133 uLogger.error("FIGI is not defined!") 1134 raise Exception("Ticker or FIGI required") 1135 1136 else: 1137 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1138 1139 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1140 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1141 self.body = str({"figi": self.figi, "depth": self.depth}) 1142 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") 1143 1144 if pricesResponse: 1145 # list of dicts with sellers orders: 1146 prices["buy"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1147 1148 # list of dicts with buyers orders: 1149 prices["sell"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1150 1151 # max price of instrument at this time: 1152 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1153 1154 # min price of instrument at this time: 1155 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1156 1157 # last price of deal with instrument: 1158 prices["lastPrice"] = NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]) if "lastPrice" in pricesResponse.keys() else 0 1159 1160 # last close price of instrument: 1161 prices["closePrice"] = NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]) if "closePrice" in pricesResponse.keys() else 0 1162 1163 else: 1164 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1165 uLogger.debug("Server response: {}".format(pricesResponse)) 1166 1167 if show: 1168 if prices["buy"] or prices["sell"]: 1169 info = [ 1170 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1171 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1172 self.ticker, 1173 self.figi, 1174 self.depth, 1175 ), 1176 uLog.sepShort, "\n", 1177 " Orders of Buyers | Orders of Sellers\n", 1178 uLog.sepShort, "\n", 1179 " Sell prices (vol.) | Buy prices (vol.)\n", 1180 uLog.sepShort, "\n", 1181 ] 1182 1183 if not prices["buy"]: 1184 info.append(" | No orders!\n") 1185 sumBuy = 0 1186 1187 else: 1188 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1189 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1190 for item in maxMinSorted: 1191 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1192 1193 if not prices["sell"]: 1194 info.append("No orders! |\n") 1195 sumSell = 0 1196 1197 else: 1198 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1199 for item in prices["sell"]: 1200 info.append("{:>19} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1201 1202 info.extend([ 1203 uLog.sepShort, "\n", 1204 "{:>19} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1205 uLog.sepShort, "\n", 1206 ]) 1207 1208 infoText = "".join(info) 1209 1210 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1211 1212 else: 1213 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1214 1215 return prices 1216 1217 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1218 """ 1219 This method get and show information about all available broker instruments for current user account. 1220 If `instrumentsFile` string is not empty then also save information to this file. 1221 1222 :param show: if `True` then print results to console, if `False` - print only to file. 1223 :return: multi-lines string with all available broker instruments 1224 """ 1225 if not self.iList: 1226 self.iList = self.Listing() 1227 1228 info = [ 1229 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1230 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1231 ] 1232 1233 # add instruments count by type: 1234 for iType in self.iList.keys(): 1235 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1236 1237 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1238 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1239 1240 # generating info tables with all instruments by type: 1241 for iType in self.iList.keys(): 1242 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1243 1244 for instrument in self.iList[iType].keys(): 1245 iName = self.iList[iType][instrument]["name"] # instrument's name 1246 if len(iName) > 57: 1247 iName = "{}...".format(iName[:54]) # right trim for a long string 1248 1249 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1250 self.iList[iType][instrument]["ticker"], 1251 iName, 1252 self.iList[iType][instrument]["figi"], 1253 self.iList[iType][instrument]["currency"], 1254 self.iList[iType][instrument]["lot"], 1255 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1256 )) 1257 1258 infoText = "".join(info) 1259 1260 if show: 1261 uLogger.info(infoText) 1262 1263 if self.instrumentsFile: 1264 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1265 fH.write(infoText) 1266 1267 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1268 1269 return infoText 1270 1271 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1272 """ 1273 This method search and show information about instruments by part of its ticker, FIGI or name. 1274 If `searchResultsFile` string is not empty then also save information to this file. 1275 1276 :param pattern: string with part of ticker, FIGI or instrument's name. 1277 :param show: if `True` then print results to console, if `False` - return list of result only. 1278 :return: list of dictionaries with all found instruments. 1279 """ 1280 if not self.iList: 1281 self.iList = self.Listing() 1282 1283 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1284 compiledPattern = re.compile(pattern, re.IGNORECASE) 1285 1286 for iType in self.iList: 1287 for instrument in self.iList[iType].values(): 1288 searchResult = compiledPattern.search(" ".join( 1289 [instrument["ticker"], instrument["figi"], instrument["name"]] 1290 )) 1291 1292 if searchResult: 1293 searchResults[iType][instrument["ticker"]] = instrument 1294 1295 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1296 info = [ 1297 "# Search results\n\n", 1298 "* **Search pattern:** [{}]\n".format(pattern), 1299 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1300 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1301 ] 1302 infoShort = info[:] 1303 1304 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1305 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1306 skippedLine = "| ... | ... | ... | ... |\n" 1307 1308 if resultsLen == 0: 1309 info.append("\nNo results\n") 1310 infoShort.append("\nNo results\n") 1311 uLogger.warning("No results. Try changing your search pattern.") 1312 1313 else: 1314 for iType in searchResults: 1315 iTypeValuesCount = len(searchResults[iType].values()) 1316 if iTypeValuesCount > 0: 1317 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1318 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1319 1320 for instrument in searchResults[iType].values(): 1321 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1322 instrument["type"], 1323 instrument["ticker"], 1324 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1325 instrument["figi"], 1326 )) 1327 1328 if iTypeValuesCount <= 5: 1329 infoShort.extend(info[-iTypeValuesCount:]) 1330 1331 else: 1332 infoShort.extend(info[-5:]) 1333 infoShort.append(skippedLine) 1334 1335 infoText = "".join(info) 1336 infoTextShort = "".join(infoShort) 1337 1338 if show: 1339 uLogger.info(infoTextShort) 1340 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1341 1342 if self.searchResultsFile: 1343 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1344 fH.write(infoText) 1345 1346 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1347 1348 return searchResults 1349 1350 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1351 """ 1352 Creating list with unique instrument FIGIs from input list of tickers or FIGIs. 1353 1354 :param instruments: list of strings with tickers or FIGIs. 1355 :return: list with unique instrument FIGIs only. 1356 """ 1357 requestedInstruments = [] 1358 for iName in instruments: 1359 if iName not in self.aliases.keys(): 1360 if iName not in requestedInstruments: 1361 requestedInstruments.append(iName) 1362 1363 else: 1364 if iName not in requestedInstruments: 1365 if self.aliases[iName] not in requestedInstruments: 1366 requestedInstruments.append(self.aliases[iName]) 1367 1368 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1369 1370 onlyUniqueFIGIs = [] 1371 for iName in requestedInstruments: 1372 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1373 continue 1374 1375 self.ticker = iName 1376 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1377 1378 if not iData: 1379 self.ticker = "" 1380 self.figi = iName 1381 1382 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1383 1384 if not iData: 1385 self.figi = "" 1386 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1387 1388 if iData and iData["figi"] not in onlyUniqueFIGIs: 1389 onlyUniqueFIGIs.append(iData["figi"]) 1390 1391 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1392 1393 return onlyUniqueFIGIs 1394 1395 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1396 """ 1397 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1398 See limits: https://tinkoff.github.io/investAPI/limits/ 1399 If `pricesFile` string is not empty then also save information to this file. 1400 1401 :param instruments: list of strings with tickers or FIGIs. 1402 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1403 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1404 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1405 """ 1406 if instruments is None or not instruments: 1407 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1408 raise Exception("Ticker or FIGI required") 1409 1410 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1411 1412 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1413 1414 iList = [] # trying to get info and current prices about all unique instruments: 1415 for self.figi in onlyUniqueFIGIs: 1416 iData = self.SearchByFIGI(requestPrice=True) 1417 iList.append(iData) 1418 1419 self.ShowListOfPrices(iList, show) 1420 1421 return iList 1422 1423 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1424 """ 1425 Show table contains current prices of given instruments. 1426 1427 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1428 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1429 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1430 :return: multilines text in Markdown format as a table contains current prices. 1431 """ 1432 infoText = "" 1433 1434 if show or self.pricesFile: 1435 info = [ 1436 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1437 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1438 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1439 ] 1440 1441 for item in iList: 1442 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1443 item["ticker"], 1444 item["figi"], 1445 item["type"], 1446 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1447 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1448 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1449 "{} / {}".format( 1450 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1451 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1452 ), 1453 "{} / {}".format( 1454 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1455 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1456 ), 1457 item["currency"], 1458 )) 1459 1460 infoText = "".join(info) 1461 1462 if show: 1463 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1464 1465 if self.pricesFile: 1466 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1467 fH.write(infoText) 1468 1469 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1470 1471 return infoText 1472 1473 def RequestTradingStatus(self) -> dict: 1474 """ 1475 Requesting trading status for the instrument defined by `figi` variable. 1476 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1477 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1478 1479 :return: dictionary with trading status attributes. Response example: 1480 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1481 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1482 """ 1483 if self.figi is None or not self.figi: 1484 uLogger.error("Variable `figi` must be defined for using this method!") 1485 raise Exception("FIGI required") 1486 1487 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1488 1489 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1490 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1491 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1492 1493 uLogger.debug("Records about current trading status successfully received") 1494 1495 return tradingStatus 1496 1497 def RequestPortfolio(self) -> dict: 1498 """ 1499 Requesting actual user's portfolio for current `accountId`. 1500 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1501 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1502 1503 :return: dictionary with user's portfolio. 1504 """ 1505 if self.accountId is None or not self.accountId: 1506 uLogger.error("Variable `accountId` must be defined for using this method!") 1507 raise Exception("Account ID required") 1508 1509 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1510 1511 self.body = str({"accountId": self.accountId}) 1512 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1513 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1514 1515 uLogger.debug("Records about user's portfolio successfully received") 1516 1517 return rawPortfolio 1518 1519 def RequestPositions(self) -> dict: 1520 """ 1521 Requesting open positions by currencies and instruments for current `accountId`. 1522 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1523 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1524 1525 :return: dictionary with open positions by instruments. 1526 """ 1527 if self.accountId is None or not self.accountId: 1528 uLogger.error("Variable `accountId` must be defined for using this method!") 1529 raise Exception("Account ID required") 1530 1531 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1532 1533 self.body = str({"accountId": self.accountId}) 1534 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1535 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1536 1537 uLogger.debug("Records about current open positions successfully received") 1538 1539 return rawPositions 1540 1541 def RequestPendingOrders(self) -> list: 1542 """ 1543 Requesting current actual pending orders for current `accountId`. 1544 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1545 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1546 1547 :return: list of dictionaries with pending orders. 1548 """ 1549 if self.accountId is None or not self.accountId: 1550 uLogger.error("Variable `accountId` must be defined for using this method!") 1551 raise Exception("Account ID required") 1552 1553 uLogger.debug("Requesting current actual pending orders. Wait, please...") 1554 1555 self.body = str({"accountId": self.accountId}) 1556 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1557 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1558 1559 uLogger.debug("[{}] records about pending orders received".format(len(rawOrders))) 1560 1561 return rawOrders 1562 1563 def RequestStopOrders(self) -> list: 1564 """ 1565 Requesting current actual stop orders for current `accountId`. 1566 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1567 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1568 1569 :return: list of dictionaries with stop orders. 1570 """ 1571 if self.accountId is None or not self.accountId: 1572 uLogger.error("Variable `accountId` must be defined for using this method!") 1573 raise Exception("Account ID required") 1574 1575 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1576 1577 self.body = str({"accountId": self.accountId}) 1578 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1579 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1580 1581 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1582 1583 return rawStopOrders 1584 1585 def Overview(self, show: bool = False, details: str = "full") -> dict: 1586 """ 1587 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1588 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1589 are defined then also save information to file. 1590 1591 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1592 many requests about the state of the portfolio, and then, based on the received data, a large number 1593 of calculation and statistics are collected. 1594 1595 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1596 :param details: how detailed should the information be? You should specify one of strings: 1597 `full` - shows full available information about portfolio status (by default), 1598 `positions` - shows only open positions, 1599 `digest` - show a short digest of the portfolio status, 1600 `analytics` - shows only the analytics section and the distribution of the portfolio by various categories, 1601 `orders` - shows only sections of open limits and stop orders. 1602 :return: dictionary with client's raw portfolio and some statistics. 1603 """ 1604 if self.accountId is None or not self.accountId: 1605 uLogger.error("Variable `accountId` must be defined for using this method!") 1606 raise Exception("Account ID required") 1607 1608 view = { 1609 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1610 "headers": {}, # list of dictionaries, response headers without "positions" section 1611 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1612 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1613 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1614 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1615 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1616 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1617 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1618 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1619 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1620 }, 1621 "stat": { # --- some statistics calculated using "raw" sections: 1622 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1623 "availableRUB": 0., # available rubles (without other currencies) 1624 "blockedRUB": 0., # blocked sum in Russian Rouble 1625 "totalChangesRUB": 0., # changes for all open trades in RUB 1626 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1627 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1628 "sharesCostRUB": 0., # costs of all shares in RUB 1629 "bondsCostRUB": 0., # costs of all bonds in RUB 1630 "etfsCostRUB": 0., # costs of all etfs in RUB 1631 "futuresCostRUB": 0., # costs of all futures in RUB 1632 "Currencies": [], # list of dictionaries of all currencies statistics 1633 "Shares": [], # list of dictionaries of all shares statistics 1634 "Bonds": [], # list of dictionaries of all bonds statistics 1635 "Etfs": [], # list of dictionaries of all etfs statistics 1636 "Futures": [], # list of dictionaries of all futures statistics 1637 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1638 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1639 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1640 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1641 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1642 }, 1643 "analytics": { # --- some analytics of portfolio: 1644 "distrByAssets": {}, # portfolio distribution by assets 1645 "distrByCompanies": {}, # portfolio distribution by companies 1646 "distrBySectors": {}, # portfolio distribution by sectors 1647 "distrByCurrencies": {}, # portfolio distribution by currencies 1648 "distrByCountries": {}, # portfolio distribution by countries 1649 } 1650 } 1651 1652 details = details.lower() 1653 availableDetails = ["full", "positions", "digest", "analytics", "orders"] 1654 if details not in availableDetails: 1655 details = "full" 1656 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1657 1658 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1659 1660 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1661 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1662 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending orders (list) 1663 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1664 1665 # save response headers without "positions" section: 1666 for key in portfolioResponse.keys(): 1667 if key != "positions": 1668 view["raw"]["headers"][key] = portfolioResponse[key] 1669 1670 else: 1671 continue 1672 1673 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1674 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1675 for item in portfolioResponse["positions"]: 1676 if item["instrumentType"] == "currency": 1677 self.figi = item["figi"] 1678 curr = self.SearchByFIGI(requestPrice=False) 1679 1680 # current price of currency in RUB: 1681 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1682 "name": curr["name"], 1683 "currentPrice": NanoToFloat( 1684 item["currentPrice"]["units"], 1685 item["currentPrice"]["nano"] 1686 ), 1687 } 1688 1689 view["raw"]["Currencies"].append(item) 1690 1691 elif item["instrumentType"] == "share": 1692 view["raw"]["Shares"].append(item) 1693 1694 elif item["instrumentType"] == "bond": 1695 view["raw"]["Bonds"].append(item) 1696 1697 elif item["instrumentType"] == "etf": 1698 view["raw"]["Etfs"].append(item) 1699 1700 elif item["instrumentType"] == "futures": 1701 view["raw"]["Futures"].append(item) 1702 1703 else: 1704 continue 1705 1706 # how many volume of currencies (by ISO currency name) are blocked: 1707 for item in view["raw"]["positions"]["blocked"]: 1708 blocked = NanoToFloat(item["units"], item["nano"]) 1709 if blocked > 0: 1710 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1711 1712 # how many volume of instruments (by FIGI) are blocked: 1713 for item in view["raw"]["positions"]["securities"]: 1714 blocked = int(item["blocked"]) 1715 if blocked > 0: 1716 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1717 1718 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1719 1720 if "rub" in allBlocked.keys(): 1721 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1722 1723 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1724 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1725 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1726 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1727 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1728 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1729 view["stat"]["portfolioCostRUB"] = sum([ 1730 view["stat"]["allCurrenciesCostRUB"], 1731 view["stat"]["sharesCostRUB"], 1732 view["stat"]["bondsCostRUB"], 1733 view["stat"]["etfsCostRUB"], 1734 view["stat"]["futuresCostRUB"], 1735 ]) 1736 1737 # --- calculating some portfolio statistics: 1738 byComp = {} # distribution by companies 1739 bySect = {} # distribution by sectors 1740 byCurr = {} # distribution by currencies (include RUB) 1741 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1742 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1743 1744 for item in portfolioResponse["positions"]: 1745 self.figi = item["figi"] 1746 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1747 1748 if instrument: 1749 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1750 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1751 1752 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1753 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1754 1755 else: 1756 blocked = 0 1757 1758 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1759 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1760 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1761 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1762 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1763 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1764 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1765 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1766 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1767 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1768 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1769 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1770 1771 statData = { 1772 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1773 "ticker": instrument["ticker"], # ticker by FIGI 1774 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1775 "volume": volume, # available volume of instrument 1776 "lots": lots, # volume in lots of instrument 1777 "direction": direction, # direction of an instrument's position: short or long 1778 "blocked": blocked, # blocked volume of currency or instrument 1779 "currentPrice": curPrice, # current instrument's price in basic asset 1780 "average": average, # current average position price 1781 "cost": cost, # current cost of all volume of instrument in basic asset 1782 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1783 "costRUB": costRUB, # cost of instrument in ruble 1784 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1785 "profit": profit, # expected profit at current moment 1786 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1787 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1788 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1789 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1790 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1791 "step": instrument["step"], # minimum price increment 1792 } 1793 1794 # adding distribution by unique countries: 1795 if statData["country"] not in byCountry.keys(): 1796 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1797 1798 else: 1799 byCountry[statData["country"]]["cost"] += costRUB 1800 byCountry[statData["country"]]["percent"] += percentCostRUB 1801 1802 if item["instrumentType"] != "currency": 1803 # adding distribution by unique companies: 1804 if statData["name"]: 1805 if statData["name"] not in byComp.keys(): 1806 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1807 1808 else: 1809 byComp[statData["name"]]["cost"] += costRUB 1810 byComp[statData["name"]]["percent"] += percentCostRUB 1811 1812 # adding distribution by unique sectors: 1813 if statData["sector"] not in bySect.keys(): 1814 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1815 1816 else: 1817 bySect[statData["sector"]]["cost"] += costRUB 1818 bySect[statData["sector"]]["percent"] += percentCostRUB 1819 1820 # adding distribution by unique currencies: 1821 if currency not in byCurr.keys(): 1822 byCurr[currency] = { 1823 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1824 "cost": costRUB, 1825 "percent": percentCostRUB 1826 } 1827 1828 else: 1829 byCurr[currency]["cost"] += costRUB 1830 byCurr[currency]["percent"] += percentCostRUB 1831 1832 # saving statistics for every instrument: 1833 if item["instrumentType"] == "currency": 1834 view["stat"]["Currencies"].append(statData) 1835 1836 # update dict with free funds for trading (total - blocked) by currencies 1837 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1838 view["stat"]["funds"][currency] = { 1839 "total": volume, 1840 "totalCostRUB": costRUB, # total volume cost in rubles 1841 "free": volume - blocked, 1842 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1843 } 1844 1845 elif item["instrumentType"] == "share": 1846 view["stat"]["Shares"].append(statData) 1847 1848 elif item["instrumentType"] == "bond": 1849 view["stat"]["Bonds"].append(statData) 1850 1851 elif item["instrumentType"] == "etf": 1852 view["stat"]["Etfs"].append(statData) 1853 1854 elif item["instrumentType"] == "Futures": 1855 view["stat"]["Futures"].append(statData) 1856 1857 else: 1858 continue 1859 1860 # total changes in Russian Ruble: 1861 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1862 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1863 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1864 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1865 view["stat"]["funds"]["rub"] = { 1866 "total": view["stat"]["availableRUB"], 1867 "totalCostRUB": view["stat"]["availableRUB"], 1868 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1869 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1870 } 1871 1872 # --- pending orders sector data: 1873 uniquePendingOrders = [] 1874 uniquePendingOrdersFIGIs = [] 1875 for item in view["raw"]["orders"]: 1876 if item["figi"] not in uniquePendingOrdersFIGIs: 1877 uniquePendingOrdersFIGIs.append(item["figi"]) 1878 uniquePendingOrders.append(item) 1879 1880 for item in uniquePendingOrders: 1881 self.figi = item["figi"] 1882 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1883 1884 if instrument: 1885 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1886 orderType = TKS_ORDER_TYPES[item["orderType"]] 1887 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1888 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1889 1890 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1891 if item["direction"] == "ORDER_DIRECTION_BUY": 1892 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1893 1894 else: 1895 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1896 1897 # requested price for order execution: 1898 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1899 1900 # necessary changes in percent to reach target from current price: 1901 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1902 1903 view["stat"]["orders"].append({ 1904 "orderID": item["orderId"], # orderId number parameter of current order 1905 "figi": item["figi"], # FIGI identification 1906 "ticker": instrument["ticker"], # ticker name by FIGI 1907 "lotsRequested": item["lotsRequested"], # requested lots value 1908 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1909 "currentPrice": lastPrice, # current instrument's price for defined action 1910 "targetPrice": target, # requested price for order execution in base currency 1911 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1912 "percentChanges": changes, # changes in percent to target from current price 1913 "currency": item["currency"], # instrument's currency name 1914 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1915 "type": orderType, # type of order from TKS_ORDER_TYPES 1916 "status": orderState, # order status from TKS_ORDER_STATES 1917 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1918 }) 1919 1920 # --- stop orders sector data: 1921 uniqueStopOrders = [] 1922 uniqueStopOrdersFIGIs = [] 1923 for item in view["raw"]["stopOrders"]: 1924 if item["figi"] not in uniqueStopOrdersFIGIs: 1925 uniqueStopOrdersFIGIs.append(item["figi"]) 1926 uniqueStopOrders.append(item) 1927 1928 for item in uniqueStopOrders: 1929 self.figi = item["figi"] 1930 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1931 1932 if instrument: 1933 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1934 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1935 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1936 1937 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1938 if "expirationTime" in item.keys(): 1939 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1940 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1941 1942 else: 1943 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1944 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1945 1946 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1947 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1948 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1949 1950 else: 1951 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1952 1953 # requested price when stop-order executed: 1954 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1955 1956 # price for limit-order, set up when stop-order executed: 1957 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1958 1959 # necessary changes in percent to reach target from current price: 1960 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1961 1962 view["stat"]["stopOrders"].append({ 1963 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1964 "figi": item["figi"], # FIGI identification 1965 "ticker": instrument["ticker"], # ticker name by FIGI 1966 "lotsRequested": item["lotsRequested"], # requested lots value 1967 "currentPrice": lastPrice, # current instrument's price for defined action 1968 "targetPrice": target, # requested price for stop-order execution in base currency 1969 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1970 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1971 "percentChanges": changes, # changes in percent to target from current price 1972 "currency": item["currency"], # instrument's currency name 1973 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1974 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 1975 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 1976 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 1977 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 1978 }) 1979 1980 # --- calculating data for analytics section: 1981 # portfolio distribution by assets: 1982 view["analytics"]["distrByAssets"] = { 1983 "Ruble": { 1984 "uniques": 1, 1985 "cost": view["stat"]["availableRUB"], 1986 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1987 }, 1988 "Currencies": { 1989 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 1990 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 1991 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1992 }, 1993 "Shares": { 1994 "uniques": len(view["stat"]["Shares"]), 1995 "cost": view["stat"]["sharesCostRUB"], 1996 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1997 }, 1998 "Bonds": { 1999 "uniques": len(view["stat"]["Bonds"]), 2000 "cost": view["stat"]["bondsCostRUB"], 2001 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2002 }, 2003 "Etfs": { 2004 "uniques": len(view["stat"]["Etfs"]), 2005 "cost": view["stat"]["etfsCostRUB"], 2006 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2007 }, 2008 "Futures": { 2009 "uniques": len(view["stat"]["Futures"]), 2010 "cost": view["stat"]["futuresCostRUB"], 2011 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2012 }, 2013 } 2014 2015 # portfolio distribution by companies: 2016 view["analytics"]["distrByCompanies"]["All money cash"] = { 2017 "ticker": "", 2018 "cost": view["stat"]["allCurrenciesCostRUB"], 2019 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2020 } 2021 view["analytics"]["distrByCompanies"].update(byComp) 2022 2023 # portfolio distribution by sectors: 2024 view["analytics"]["distrBySectors"]["All money cash"] = { 2025 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2026 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2027 } 2028 view["analytics"]["distrBySectors"].update(bySect) 2029 2030 # portfolio distribution by currencies: 2031 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2032 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2033 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2034 2035 view["analytics"]["distrByCurrencies"].update(byCurr) 2036 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2037 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2038 2039 # portfolio distribution by countries: 2040 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2041 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2042 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2043 2044 view["analytics"]["distrByCountries"].update(byCountry) 2045 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2046 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2047 2048 # --- Prepare text statistics overview in human-readable: 2049 if show: 2050 # Whatever the value `details`, header not changes: 2051 info = [ 2052 "# Client's portfolio\n\n", 2053 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 2054 "* **Account ID:** [{}]\n".format(self.accountId), 2055 ] 2056 2057 if details in ["full", "positions", "digest"]: 2058 info.extend([ 2059 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2060 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2061 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2062 view["stat"]["totalChangesRUB"], 2063 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2064 view["stat"]["totalChangesPercentRUB"], 2065 ), 2066 ]) 2067 2068 if details in ["full", "positions"]: 2069 info.extend([ 2070 "## Open positions\n\n", 2071 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2072 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2073 "| Ruble | {:>31} | | | | | |\n".format( 2074 "{:.2f} ({:.2f}) rub".format( 2075 view["stat"]["availableRUB"], 2076 view["stat"]["blockedRUB"], 2077 ) 2078 ) 2079 ]) 2080 2081 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2082 return [ 2083 "| | | | | | | |\n", 2084 "| {:<27} | | | | | {:>19} | |\n".format( 2085 noTradeStr if noTradeStr else typeStr, 2086 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2087 ), 2088 ] 2089 2090 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2091 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2092 "{} [{}]".format(data["ticker"], data["figi"]), 2093 "{:.2f} ({:.2f}) {}".format( 2094 data["volume"], 2095 data["blocked"], 2096 data["currency"], 2097 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2098 data["volume"], 2099 data["blocked"], 2100 ), 2101 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2102 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2103 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2104 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2105 "{}{:.2f} {} ({}{:.2f}%)".format( 2106 "+" if data["profit"] > 0 else "", 2107 data["profit"], data["baseCurrencyName"], 2108 "+" if data["percentProfit"] > 0 else "", 2109 data["percentProfit"], 2110 ), 2111 ) 2112 2113 # --- Show currencies section: 2114 if view["stat"]["Currencies"]: 2115 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2116 for item in view["stat"]["Currencies"]: 2117 info.append(_InfoStr(item, showCurrencyName=True)) 2118 2119 else: 2120 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2121 2122 # --- Show shares section: 2123 if view["stat"]["Shares"]: 2124 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2125 2126 for item in view["stat"]["Shares"]: 2127 info.append(_InfoStr(item)) 2128 2129 else: 2130 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2131 2132 # --- Show bonds section: 2133 if view["stat"]["Bonds"]: 2134 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2135 2136 for item in view["stat"]["Bonds"]: 2137 info.append(_InfoStr(item)) 2138 2139 else: 2140 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2141 2142 # --- Show etfs section: 2143 if view["stat"]["Etfs"]: 2144 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2145 2146 for item in view["stat"]["Etfs"]: 2147 info.append(_InfoStr(item)) 2148 2149 else: 2150 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2151 2152 # --- Show futures section: 2153 if view["stat"]["Futures"]: 2154 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2155 2156 for item in view["stat"]["Futures"]: 2157 info.append(_InfoStr(item)) 2158 2159 else: 2160 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2161 2162 if details in ["full", "orders"]: 2163 # --- Show pending orders section: 2164 if view["stat"]["orders"]: 2165 info.extend([ 2166 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2167 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2168 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2169 ]) 2170 2171 for item in view["stat"]["orders"]: 2172 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2173 "{} [{}]".format(item["ticker"], item["figi"]), 2174 item["orderID"], 2175 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2176 "{} {} ({}{:.2f}%)".format( 2177 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2178 item["baseCurrencyName"], 2179 "+" if item["percentChanges"] > 0 else "", 2180 float(item["percentChanges"]), 2181 ), 2182 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2183 item["action"], 2184 item["type"], 2185 item["date"], 2186 )) 2187 2188 else: 2189 info.append("\n## Total pending limit-orders: 0\n") 2190 2191 # --- Show stop orders section: 2192 if view["stat"]["stopOrders"]: 2193 info.extend([ 2194 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2195 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2196 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2197 ]) 2198 2199 for item in view["stat"]["stopOrders"]: 2200 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2201 "{} [{}]".format(item["ticker"], item["figi"]), 2202 item["orderID"], 2203 item["lotsRequested"], 2204 "{} {} ({}{:.2f}%)".format( 2205 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2206 item["baseCurrencyName"], 2207 "+" if item["percentChanges"] > 0 else "", 2208 float(item["percentChanges"]), 2209 ), 2210 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2211 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2212 item["action"], 2213 item["type"], 2214 item["expType"], 2215 item["createDate"], 2216 item["expDate"], 2217 )) 2218 2219 else: 2220 info.append("\n## Total stop-orders: 0\n") 2221 2222 if details in ["full", "analytics"]: 2223 # -- Show analytics section: 2224 if view["stat"]["portfolioCostRUB"] > 0: 2225 info.extend([ 2226 "\n# Analytics\n" 2227 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2228 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2229 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2230 view["stat"]["totalChangesRUB"], 2231 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2232 view["stat"]["totalChangesPercentRUB"], 2233 ), 2234 "\n## Portfolio distribution by assets\n" 2235 "\n| Type | Uniques | Percent | Current cost |\n", 2236 "|------------|---------|---------|--------------------|\n", 2237 ]) 2238 2239 for key in view["analytics"]["distrByAssets"].keys(): 2240 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2241 info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format( 2242 key, 2243 view["analytics"]["distrByAssets"][key]["uniques"], 2244 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2245 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2246 )) 2247 2248 maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()]) 2249 info.extend([ 2250 "\n## Portfolio distribution by companies\n" 2251 "\n| Company{} | Percent | Current cost |\n".format(" " * (maxLenNames - 7)), 2252 "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)), 2253 ]) 2254 2255 for company in view["analytics"]["distrByCompanies"].keys(): 2256 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2257 nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) 2258 info.append("| {} | {:<7} | {:<18} |\n".format( 2259 "{}{}{}".format( 2260 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2261 company, 2262 "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)), 2263 ), 2264 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2265 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2266 )) 2267 2268 maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()]) 2269 info.extend([ 2270 "\n## Portfolio distribution by sectors\n" 2271 "\n| Sector{} | Percent | Current cost |\n".format(" " * (maxLenSectors - 6)), 2272 "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)), 2273 ]) 2274 2275 for sector in view["analytics"]["distrBySectors"].keys(): 2276 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2277 info.append("| {}{} | {:<7} | {:<18} |\n".format( 2278 sector, 2279 "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)), 2280 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2281 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2282 )) 2283 2284 maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()]) 2285 info.extend([ 2286 "\n## Portfolio distribution by currencies\n" 2287 "\n| Instruments currencies{} | Percent | Current cost |\n".format(" " * (maxLenMoney - 22)), 2288 "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)), 2289 ]) 2290 2291 for curr in view["analytics"]["distrByCurrencies"].keys(): 2292 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2293 nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"]) 2294 info.append("| {} | {:<7} | {:<18} |\n".format( 2295 "[{}] {}{}".format( 2296 curr, 2297 view["analytics"]["distrByCurrencies"][curr]["name"], 2298 "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen), 2299 ), 2300 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2301 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2302 )) 2303 2304 maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()])) 2305 info.extend([ 2306 "\n## Portfolio distribution by countries\n" 2307 "\n| Assets by country{} | Percent | Current cost |\n".format(" " * (maxLenCountry - 17)), 2308 "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)), 2309 ]) 2310 2311 for country in view["analytics"]["distrByCountries"].keys(): 2312 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2313 nameLen = len(country) 2314 info.append("| {} | {:<7} | {:<18} |\n".format( 2315 "{}{}".format( 2316 country, 2317 "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen), 2318 ), 2319 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2320 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2321 )) 2322 2323 infoText = "".join(info) 2324 2325 uLogger.info(infoText) 2326 2327 if details == "full" and self.overviewFile: 2328 filename = self.overviewFile 2329 2330 elif details == "digest" and self.overviewDigestFile: 2331 filename = self.overviewDigestFile 2332 2333 elif details == "positions" and self.overviewPositionsFile: 2334 filename = self.overviewPositionsFile 2335 2336 elif details == "orders" and self.overviewOrdersFile: 2337 filename = self.overviewOrdersFile 2338 2339 elif details == "analytics" and self.overviewAnalyticsFile: 2340 filename = self.overviewAnalyticsFile 2341 2342 else: 2343 filename = "" 2344 2345 if filename: 2346 with open(filename, "w", encoding="UTF-8") as fH: 2347 fH.write(infoText) 2348 2349 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2350 2351 return view 2352 2353 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple: 2354 """ 2355 Returns history operations between two given dates for current `accountId`. 2356 If `reportFile` string is not empty then also save human-readable report. 2357 Shows some statistical data of closed positions. 2358 2359 :param start: see docstring in `GetDatesAsString()` method 2360 :param end: see docstring in `GetDatesAsString()` method 2361 :param show: if `True` then also prints all records to the console. 2362 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2363 :return: original list of dictionaries with history of deals records from API ("operations" key): 2364 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2365 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2366 """ 2367 if self.accountId is None or not self.accountId: 2368 uLogger.error("Variable `accountId` must be defined for using this method!") 2369 raise Exception("Account ID required") 2370 2371 startDate, endDate = GetDatesAsString(start, end) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2372 2373 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2374 2375 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2376 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2377 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2378 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2379 customStat = {} # custom statistics in additional to responseJSON 2380 2381 # --- output report in human-readable format: 2382 if show or self.reportFile: 2383 splitLine1 = "| | | | | |\n" # Summary section 2384 splitLine2 = "| | | | | | | | |\n" # Operations section 2385 nextDay = "" 2386 2387 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2388 2389 if len(ops) > 0: 2390 customStat = { 2391 "opsCount": 0, # total operations count 2392 "buyCount": 0, # buy operations 2393 "sellCount": 0, # sell operations 2394 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2395 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2396 "payIn": {"rub": 0.}, # Deposit brokerage account 2397 "payOut": {"rub": 0.}, # Withdrawals 2398 "divs": {"rub": 0.}, # Dividends income 2399 "coupons": {"rub": 0.}, # Coupon's income 2400 "brokerCom": {"rub": 0.}, # Service commissions 2401 "serviceCom": {"rub": 0.}, # Service commissions 2402 "marginCom": {"rub": 0.}, # Margin commissions 2403 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2404 } 2405 2406 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2407 for item in ops: 2408 if item["state"] == "OPERATION_STATE_EXECUTED": 2409 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2410 2411 # count buy operations: 2412 if "_BUY" in item["operationType"]: 2413 customStat["buyCount"] += 1 2414 2415 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2416 customStat["buyTotal"][item["payment"]["currency"]] += payment 2417 2418 else: 2419 customStat["buyTotal"][item["payment"]["currency"]] = payment 2420 2421 # count sell operations: 2422 elif "_SELL" in item["operationType"]: 2423 customStat["sellCount"] += 1 2424 2425 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2426 customStat["sellTotal"][item["payment"]["currency"]] += payment 2427 2428 else: 2429 customStat["sellTotal"][item["payment"]["currency"]] = payment 2430 2431 # count incoming operations: 2432 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2433 if item["payment"]["currency"] in customStat["payIn"].keys(): 2434 customStat["payIn"][item["payment"]["currency"]] += payment 2435 2436 else: 2437 customStat["payIn"][item["payment"]["currency"]] = payment 2438 2439 # count withdrawals operations: 2440 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2441 if item["payment"]["currency"] in customStat["payOut"].keys(): 2442 customStat["payOut"][item["payment"]["currency"]] += payment 2443 2444 else: 2445 customStat["payOut"][item["payment"]["currency"]] = payment 2446 2447 # count dividends income: 2448 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2449 if item["payment"]["currency"] in customStat["divs"].keys(): 2450 customStat["divs"][item["payment"]["currency"]] += payment 2451 2452 else: 2453 customStat["divs"][item["payment"]["currency"]] = payment 2454 2455 # count coupon's income: 2456 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2457 if item["payment"]["currency"] in customStat["coupons"].keys(): 2458 customStat["coupons"][item["payment"]["currency"]] += payment 2459 2460 else: 2461 customStat["coupons"][item["payment"]["currency"]] = payment 2462 2463 # count broker commissions: 2464 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2465 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2466 customStat["brokerCom"][item["payment"]["currency"]] += payment 2467 2468 else: 2469 customStat["brokerCom"][item["payment"]["currency"]] = payment 2470 2471 # count service commissions: 2472 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2473 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2474 customStat["serviceCom"][item["payment"]["currency"]] += payment 2475 2476 else: 2477 customStat["serviceCom"][item["payment"]["currency"]] = payment 2478 2479 # count margin commissions: 2480 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2481 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2482 customStat["marginCom"][item["payment"]["currency"]] += payment 2483 2484 else: 2485 customStat["marginCom"][item["payment"]["currency"]] = payment 2486 2487 # count withholding taxes: 2488 elif "_TAX" in item["operationType"]: 2489 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2490 customStat["allTaxes"][item["payment"]["currency"]] += payment 2491 2492 else: 2493 customStat["allTaxes"][item["payment"]["currency"]] = payment 2494 2495 else: 2496 continue 2497 2498 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2499 2500 # --- view "Actions" lines: 2501 info.extend([ 2502 "| 1 | 2 | 3 | 4 | 5 |\n", 2503 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2504 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2505 "| | Buy: {:<22} | {:<28} | | |\n".format( 2506 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2507 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2508 ), 2509 "| | Sell: {:<21} | {:<28} | | |\n".format( 2510 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2511 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2512 ), 2513 ]) 2514 2515 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2516 for key in opsKeys: 2517 if key == "rub": 2518 continue 2519 2520 info.extend([ 2521 "| | | {:<28} | | |\n".format( 2522 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2523 ), 2524 "| | | {:<28} | | |\n".format( 2525 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2526 ), 2527 ]) 2528 2529 info.append(splitLine1) 2530 2531 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2532 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2533 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2534 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2535 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2536 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2537 ) 2538 2539 # --- view "Payments" lines: 2540 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2541 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2542 2543 for key in paymentsKeys: 2544 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2545 2546 info.append(splitLine1) 2547 2548 # --- view "Commissions and taxes" lines: 2549 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2550 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2551 2552 for key in comKeys: 2553 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2554 2555 info.append(splitLine1) 2556 2557 info.extend([ 2558 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2559 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2560 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2561 ]) 2562 2563 else: 2564 info.append("Broker returned no operations during this period\n") 2565 2566 # --- view "Operations" section: 2567 for item in ops: 2568 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2569 continue 2570 2571 else: 2572 self.figi = item["figi"] if item["figi"] else "" 2573 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2574 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2575 2576 # group of deals during one day: 2577 if nextDay and item["date"].split("T")[0] != nextDay: 2578 info.append(splitLine2) 2579 nextDay = "" 2580 2581 else: 2582 nextDay = item["date"].split("T")[0] # saving current day for splitting 2583 2584 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2585 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2586 self.figi if self.figi else "—", 2587 instrument["ticker"] if instrument else "—", 2588 instrument["type"] if instrument else "—", 2589 item["quantity"] if int(item["quantity"]) > 0 else "—", 2590 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2591 TKS_OPERATION_STATES[item["state"]], 2592 TKS_OPERATION_TYPES[item["operationType"]], 2593 )) 2594 2595 infoText = "".join(info) 2596 2597 if show: 2598 uLogger.info(infoText) 2599 2600 if self.reportFile: 2601 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2602 fH.write(infoText) 2603 2604 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2605 2606 return ops, customStat 2607 2608 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2609 """ 2610 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2611 2612 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2613 Warning! Broker server used ISO UTC time by default. 2614 2615 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2616 Also, `historyFile` used to update history with `onlyMissing` parameter. 2617 2618 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2619 2620 :param start: see docstring in `GetDatesAsString()` method. 2621 :param end: see docstring in `GetDatesAsString()` method. 2622 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2623 `"hour"`, `"day"`. Default: `"hour"`. 2624 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2625 False by default. Warning! History appends only from last candle to current time 2626 with always update last candle! 2627 :param csvSep: separator if csv-file is used, `,` by default. 2628 :param show: if `True` then also prints Pandas DataFrame to the console. 2629 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2630 `["date", "time", "open", "high", "low", "close", "volume"]`. 2631 """ 2632 strStartDate, strEndDate = GetDatesAsString(start, end) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2633 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2634 history = None # empty pandas object for history 2635 2636 if interval not in TKS_CANDLE_INTERVALS.keys(): 2637 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2638 raise Exception("Incorrect value") 2639 2640 if not (self.ticker or self.figi): 2641 uLogger.error("Ticker or FIGI must be defined!") 2642 raise Exception("Ticker or FIGI required") 2643 2644 if self.ticker and not self.figi: 2645 instrumentByTicker = self.SearchByTicker(requestPrice=False, debug=False) 2646 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2647 2648 if self.figi and not self.ticker: 2649 instrumentByFIGI = self.SearchByFIGI(requestPrice=False, debug=False) 2650 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2651 2652 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2653 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2654 if interval.lower() != "day": 2655 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59 2656 2657 delta = dtEnd - dtStart # current UTC time minus last time in file 2658 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2659 2660 # calculate history length in candles: 2661 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2662 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2663 length += 1 # to avoid fraction time 2664 2665 # calculate data blocks count: 2666 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2667 2668 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2669 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2670 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2671 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2672 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2673 2674 tempOld = None # pandas object for old history, if --only-missing key present 2675 lastTime = None # datetime object of last old candle in file 2676 2677 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2678 uLogger.debug("--only-missing key present, add only last missing candles...") 2679 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2680 2681 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2682 2683 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2684 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2685 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2686 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2687 2688 # get last datetime object from last string in file or minus 1 delta if file is empty: 2689 if len(tempOld) > 0: 2690 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2691 2692 else: 2693 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2694 2695 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2696 2697 responseJSONs = [] # raw history blocks of data 2698 2699 blockEnd = dtEnd 2700 for item in range(blocks): 2701 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2702 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2703 2704 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2705 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2706 )) 2707 2708 if blockStart == blockEnd: 2709 uLogger.debug("Skipped this zero-length block...") 2710 2711 else: 2712 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2713 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2714 self.body = str({ 2715 "figi": self.figi, 2716 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2717 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2718 "interval": TKS_CANDLE_INTERVALS[interval][0] 2719 }) 2720 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1, debug=False) 2721 2722 if "code" in responseJSON.keys(): 2723 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2724 2725 else: 2726 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2727 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2728 2729 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2730 2731 blockEnd = blockStart 2732 2733 printCount = len(responseJSONs) # candles to show in console 2734 if responseJSONs: 2735 tempHistory = pd.DataFrame( 2736 data={ 2737 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2738 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2739 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2740 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2741 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2742 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2743 "volume": [int(item["volume"]) for item in responseJSONs], 2744 }, 2745 index=range(len(responseJSONs)), 2746 columns=["date", "time", "open", "high", "low", "close", "volume"], 2747 ) 2748 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2749 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2750 2751 # append only newest candles to old history if --only-missing key present: 2752 if onlyMissing and tempOld is not None and lastTime is not None: 2753 index = 0 # find start index in tempHistory data: 2754 2755 for i, item in tempHistory.iterrows(): 2756 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2757 2758 if curTime == lastTime: 2759 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2760 index = i 2761 printCount = index + 1 2762 break 2763 2764 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2765 2766 else: 2767 history = tempHistory # if no `--only-missing` key then load full data from server 2768 2769 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2770 2771 if history is not None and not history.empty: 2772 if show: 2773 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2774 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2775 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2776 )) 2777 2778 else: 2779 uLogger.warning("Received an empty candles history!") 2780 2781 if self.historyFile is not None: 2782 if history is not None and not history.empty: 2783 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2784 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2785 2786 else: 2787 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2788 2789 else: 2790 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2791 2792 return history 2793 2794 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2795 """ 2796 Load candles history from csv-file and return Pandas DataFrame object. 2797 2798 See also: `History()` and `ShowHistoryChart()` methods. 2799 2800 :param filePath: path to csv-file to open. 2801 """ 2802 loadedHistory = None # init candles data object 2803 2804 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2805 2806 if os.path.exists(filePath): 2807 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2808 2809 tfStr = self.priceModel.FormattedDelta( 2810 self.priceModel.timeframe, 2811 "{days} days {hours}h {minutes}m {seconds}s", 2812 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2813 self.priceModel.timeframe, 2814 "{hours}h {minutes}m {seconds}s", 2815 ) 2816 2817 if loadedHistory is not None and not loadedHistory.empty: 2818 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2819 len(loadedHistory), 2820 tfStr, 2821 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2822 ) 2823 2824 else: 2825 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2826 2827 else: 2828 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2829 2830 return loadedHistory 2831 2832 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2833 """ 2834 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2835 2836 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2837 Default: `index.html` (both for interact and non-interact candlesticks chart). 2838 2839 See also: `History()` and `LoadHistory()` methods. 2840 2841 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2842 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2843 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2844 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2845 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2846 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2847 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2848 """ 2849 if isinstance(candles, str): 2850 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2851 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2852 2853 elif isinstance(candles, pd.DataFrame): 2854 self.priceModel.prices = candles # set candles chain from variable 2855 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2856 2857 if "datetime" not in candles.columns: 2858 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2859 2860 else: 2861 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2862 raise Exception("Incorrect value") 2863 2864 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2865 2866 if interact: 2867 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2868 2869 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2870 2871 else: 2872 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2873 2874 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2875 2876 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2877 2878 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2879 """ 2880 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2881 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2882 2883 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2884 2885 :param operation: string "Buy" or "Sell". 2886 :param lots: volume, integer count of lots >= 1. 2887 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2888 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2889 :param expDate: string "Undefined" by default or local date in future, 2890 it is a string with format `%Y-%m-%d %H:%M:%S`. 2891 :return: JSON with response from broker server. 2892 """ 2893 if self.accountId is None or not self.accountId: 2894 uLogger.error("Variable `accountId` must be defined for using this method!") 2895 raise Exception("Account ID required") 2896 2897 if operation is None or not operation or operation not in ("Buy", "Sell"): 2898 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2899 raise Exception("Incorrect value") 2900 2901 if lots is None or lots < 1: 2902 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2903 lots = 1 2904 2905 if tp is None or tp < 0: 2906 tp = 0 2907 2908 if sl is None or sl < 0: 2909 sl = 0 2910 2911 if expDate is None or not expDate: 2912 expDate = "Undefined" 2913 2914 if not (self.ticker or self.figi): 2915 uLogger.error("Ticker or FIGI must be defined!") 2916 raise Exception("Ticker or FIGI required") 2917 2918 instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False) 2919 self.ticker = instrument["ticker"] 2920 self.figi = instrument["figi"] 2921 2922 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2923 2924 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2925 self.body = str({ 2926 "figi": self.figi, 2927 "quantity": str(lots), 2928 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2929 "accountId": str(self.accountId), 2930 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2931 }) 2932 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0, debug=False) 2933 2934 if "orderId" in response.keys(): 2935 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2936 operation, response["orderId"], 2937 self.ticker, self.figi, lots, 2938 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2939 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2940 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2941 )) 2942 2943 else: 2944 uLogger.warning("Not `oK` status received! Market order not created. See full debug log or try again and open order later.") 2945 2946 if tp > 0: 2947 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2948 2949 if sl > 0: 2950 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2951 2952 return response 2953 2954 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2955 """ 2956 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2957 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 2958 2959 See also: `Order()` and `Trade()` docstrings. 2960 2961 :param lots: volume, integer count of lots >= 1. 2962 :param tp: float > 0, take profit price of stop-order. 2963 :param sl: float > 0, stop loss price of stop-order. 2964 :param expDate: it's a local date in future. 2965 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2966 :return: JSON with response from broker server. 2967 """ 2968 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 2969 2970 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2971 """ 2972 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 2973 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2974 2975 See also: `Order()` and `Trade()` docstrings. 2976 2977 :param lots: volume, integer count of lots >= 1. 2978 :param tp: float > 0, take profit price of stop-order. 2979 :param sl: float > 0, stop loss price of stop-order. 2980 :param expDate: it's a local date in the future. 2981 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2982 :return: JSON with response from broker server. 2983 """ 2984 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 2985 2986 def CloseTrades(self, tickers: list, portfolio: dict = None) -> None: 2987 """ 2988 Close position of given instruments. 2989 2990 :param tickers: tickers list of instruments that must be closed. 2991 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 2992 This avoids unnecessary downloading data from the server. 2993 """ 2994 if not tickers: 2995 uLogger.info("Tickers list is empty, nothing to close.") 2996 2997 else: 2998 if portfolio is None or not portfolio: 2999 portfolio = self.Overview(show=False) 3000 3001 allOpenedTickers = [item["ticker"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3002 uLogger.debug("All opened instruments by it's tickers names: {}".format(allOpenedTickers)) 3003 3004 for ticker in tickers: 3005 if ticker not in allOpenedTickers: 3006 uLogger.warning("Instrument with ticker [{}] not in open positions list!".format(ticker)) 3007 continue 3008 3009 # search open trade info about instrument by ticker: 3010 instrument = {} 3011 for iType in TKS_INSTRUMENTS: 3012 if instrument: 3013 break 3014 3015 for item in portfolio["stat"][iType]: 3016 if item["ticker"] == ticker: 3017 instrument = item 3018 break 3019 3020 if instrument: 3021 self.ticker = ticker 3022 self.figi = instrument["figi"] 3023 3024 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3025 self.ticker, 3026 self.figi, 3027 int(instrument["volume"]), 3028 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3029 )) 3030 3031 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3032 3033 if tradeLots > 0: 3034 if instrument["blocked"] > 0: 3035 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3036 instrument["blocked"], 3037 self.ticker, 3038 tradeLots, 3039 )) 3040 3041 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3042 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3043 3044 else: 3045 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker)) 3046 3047 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3048 """ 3049 Close all positions of given instruments with defined type. 3050 3051 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3052 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3053 This avoids unnecessary downloading data from the server. 3054 """ 3055 if iType not in TKS_INSTRUMENTS: 3056 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3057 3058 else: 3059 if portfolio is None or not portfolio: 3060 portfolio = self.Overview(show=False) 3061 3062 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3063 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3064 3065 if tickers and portfolio: 3066 self.CloseTrades(tickers, portfolio) 3067 3068 else: 3069 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3070 3071 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3072 """ 3073 Universal method to create market or limit orders with all available parameters for current `accountId`. 3074 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3075 3076 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3077 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3078 3079 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3080 then broker immediately open market order as you can do simple --buy or --sell operations! 3081 3082 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3083 When current price will go up or down to target price value then broker opens a limit order. 3084 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3085 3086 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3087 3088 :param operation: string "Buy" or "Sell". 3089 :param orderType: string "Limit" or "Stop". 3090 :param lots: volume, integer count of lots >= 1. 3091 :param targetPrice: target price > 0. This is open trade price for limit order. 3092 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3093 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3094 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3095 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3096 Stop loss order always executed by market price. 3097 :param expDate: string "Undefined" by default or local date in future. 3098 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3099 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3100 A limit order has no expiration date, it lasts until the end of the trading day. 3101 :return: JSON with response from broker server. 3102 """ 3103 if self.accountId is None or not self.accountId: 3104 uLogger.error("Variable `accountId` must be defined for using this method!") 3105 raise Exception("Account ID required") 3106 3107 if operation is None or not operation or operation not in ("Buy", "Sell"): 3108 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3109 raise Exception("Incorrect value") 3110 3111 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3112 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3113 raise Exception("Incorrect value") 3114 3115 if lots is None or lots < 1: 3116 uLogger.error("You must define trade volume > 0: integer count of lots!") 3117 raise Exception("Incorrect value") 3118 3119 if targetPrice is None or targetPrice <= 0: 3120 uLogger.error("Target price for limit-order must be greater than 0!") 3121 raise Exception("Incorrect value") 3122 3123 if limitPrice is None or limitPrice <= 0: 3124 limitPrice = targetPrice 3125 3126 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3127 stopType = "Limit" 3128 3129 if expDate is None or not expDate: 3130 expDate = "Undefined" 3131 3132 if not (self.ticker or self.figi): 3133 uLogger.error("Tocker or FIGI must be defined!") 3134 raise Exception("Ticker or FIGI required") 3135 3136 response = {} 3137 instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False) 3138 self.ticker = instrument["ticker"] 3139 self.figi = instrument["figi"] 3140 3141 if orderType == "Limit": 3142 uLogger.debug( 3143 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3144 self.ticker, self.figi, 3145 operation, lots, targetPrice, instrument["currency"], 3146 )) 3147 3148 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3149 self.body = str({ 3150 "figi": self.figi, 3151 "quantity": str(lots), 3152 "price": FloatToNano(targetPrice), 3153 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3154 "accountId": str(self.accountId), 3155 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3156 }) 3157 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False) 3158 3159 if "orderId" in response.keys(): 3160 uLogger.info( 3161 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3162 response["orderId"], 3163 self.ticker, self.figi, 3164 operation, lots, targetPrice, instrument["currency"], 3165 )) 3166 3167 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3168 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3169 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3170 targetPrice, instrument["currency"], 3171 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3172 )) 3173 3174 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3175 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3176 targetPrice, instrument["currency"], 3177 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3178 )) 3179 3180 else: 3181 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.") 3182 3183 if orderType == "Stop": 3184 uLogger.debug( 3185 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3186 self.ticker, self.figi, 3187 operation, lots, 3188 targetPrice, instrument["currency"], 3189 limitPrice, instrument["currency"], 3190 stopType, expDate, 3191 )) 3192 3193 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3194 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3195 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3196 3197 body = { 3198 "figi": self.figi, 3199 "quantity": str(lots), 3200 "price": FloatToNano(limitPrice), 3201 "stopPrice": FloatToNano(targetPrice), 3202 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3203 "accountId": str(self.accountId), 3204 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3205 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3206 } 3207 3208 if expDateUTC: 3209 body["expireDate"] = expDateUTC 3210 3211 self.body = str(body) 3212 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False) 3213 3214 if "stopOrderId" in response.keys(): 3215 uLogger.info( 3216 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3217 response["stopOrderId"], 3218 self.ticker, self.figi, 3219 operation, lots, 3220 targetPrice, instrument["currency"], 3221 limitPrice, instrument["currency"], 3222 TKS_STOP_ORDER_TYPES[stopOrderType], 3223 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3224 )) 3225 3226 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3227 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3228 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3229 targetPrice, instrument["currency"], 3230 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3231 )) 3232 3233 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3234 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3235 targetPrice, instrument["currency"], 3236 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3237 )) 3238 3239 else: 3240 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.") 3241 3242 return response 3243 3244 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3245 """ 3246 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3247 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3248 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3249 See also: `Order()` docstring. 3250 3251 :param lots: volume, integer count of lots >= 1. 3252 :param targetPrice: target price > 0. This is open trade price for limit order. 3253 :return: JSON with response from broker server. 3254 """ 3255 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3256 3257 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3258 """ 3259 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3260 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3261 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3262 target price value then broker opens a limit order. See also: `Order()` docstring. 3263 3264 :param lots: volume, integer count of lots >= 1. 3265 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3266 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3267 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3268 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3269 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3270 :param expDate: string "Undefined" by default or local date in future. 3271 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3272 This date is converting to UTC format for server. 3273 :return: JSON with response from broker server. 3274 """ 3275 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3276 3277 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3278 """ 3279 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3280 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3281 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3282 See also: `Order()` docstring. 3283 3284 :param lots: volume, integer count of lots >= 1. 3285 :param targetPrice: target price > 0. This is open trade price for limit order. 3286 :return: JSON with response from broker server. 3287 """ 3288 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3289 3290 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3291 """ 3292 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3293 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3294 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3295 target price value then broker opens a limit order. See also: `Order()` docstring. 3296 3297 :param lots: volume, integer count of lots >= 1. 3298 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3299 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3300 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3301 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3302 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3303 :param expDate: string "Undefined" by default or local date in future. 3304 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3305 This date is converting to UTC format for server. 3306 :return: JSON with response from broker server. 3307 """ 3308 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3309 3310 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3311 """ 3312 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3313 3314 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3315 :param allOrdersIDs: pre-received lists of all active pending orders. 3316 This avoids unnecessary downloading data from the server. 3317 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3318 """ 3319 if self.accountId is None or not self.accountId: 3320 uLogger.error("Variable `accountId` must be defined for using this method!") 3321 raise Exception("Account ID required") 3322 3323 if orderIDs: 3324 if allOrdersIDs is None or not allOrdersIDs: 3325 rawOrders = self.RequestPendingOrders() 3326 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3327 3328 if allStopOrdersIDs is None or not allStopOrdersIDs: 3329 rawStopOrders = self.RequestStopOrders() 3330 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3331 3332 for orderID in orderIDs: 3333 idInPendingOrders = orderID in allOrdersIDs 3334 idInStopOrders = orderID in allStopOrdersIDs 3335 3336 if not (idInPendingOrders or idInStopOrders): 3337 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3338 continue 3339 3340 else: 3341 if idInPendingOrders: 3342 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3343 3344 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3345 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3346 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3347 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3348 3349 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3350 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3351 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3352 3353 else: 3354 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3355 3356 elif idInStopOrders: 3357 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3358 3359 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3360 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3361 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3362 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3363 3364 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3365 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3366 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3367 3368 else: 3369 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3370 3371 else: 3372 continue 3373 3374 def CloseAllOrders(self) -> None: 3375 """ 3376 Gets a list of open pending and stop orders and cancel it all. 3377 """ 3378 rawOrders = self.RequestPendingOrders() 3379 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3380 lenOrders = len(allOrdersIDs) 3381 3382 rawStopOrders = self.RequestStopOrders() 3383 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3384 lenSOrders = len(allStopOrdersIDs) 3385 3386 if lenOrders > 0 or lenSOrders > 0: 3387 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3388 3389 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3390 3391 else: 3392 uLogger.info("Orders not found, nothing to cancel.") 3393 3394 def CloseAll(self, *args) -> None: 3395 """ 3396 Close all available (not blocked) opened trades and orders. 3397 3398 Also, you can select one or more keywords case-insensitive: 3399 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3400 3401 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3402 """ 3403 overview = self.Overview(show=False) # get all open trades info 3404 3405 if len(args) == 0: 3406 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3407 self.CloseAllOrders() # close all pending and stop orders 3408 3409 for iType in TKS_INSTRUMENTS: 3410 if iType != "Currencies": 3411 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3412 3413 else: 3414 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3415 lowerArgs = [x.lower() for x in args] 3416 3417 if "orders" in lowerArgs: 3418 self.CloseAllOrders() # close all pending and stop orders 3419 3420 for iType in TKS_INSTRUMENTS: 3421 if iType.lower() in lowerArgs and iType != "Currencies": 3422 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3423 3424 @staticmethod 3425 def ParseOrderParameters(operation, **inputParameters): 3426 """ 3427 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3428 3429 :param operation: string "Buy" or "Sell". 3430 :param inputParameters: this is dict of strings that looks like this 3431 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3432 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3433 "prices" key: one or more prices to open limit-orders 3434 Counts of values in lots and prices lists must be equals! 3435 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3436 """ 3437 # TODO: update order grid work with api v2 3438 pass 3439 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3440 # 3441 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3442 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3443 # raise Exception("Incorrect value") 3444 # 3445 # if "l" in inputParameters.keys(): 3446 # inputParameters["lots"] = inputParameters.pop("l") 3447 # 3448 # if "p" in inputParameters.keys(): 3449 # inputParameters["prices"] = inputParameters.pop("p") 3450 # 3451 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3452 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3453 # raise Exception("Incorrect value") 3454 # 3455 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3456 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3457 # 3458 # if len(lots) != len(prices): 3459 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3460 # raise Exception("Incorrect value") 3461 # 3462 # uLogger.debug("Extracted parameters for orders:") 3463 # uLogger.debug("lots = {}".format(lots)) 3464 # uLogger.debug("prices = {}".format(prices)) 3465 # 3466 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3467 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3468 # uLogger.debug("Order parameters: {}".format(result)) 3469 # 3470 # return result 3471 3472 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3473 """ 3474 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3475 3476 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3477 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3478 """ 3479 result = False 3480 msg = "Instrument not defined!" 3481 3482 if portfolio is None or not portfolio: 3483 portfolio = self.Overview(show=False) 3484 3485 if self.ticker: 3486 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3487 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3488 3489 for iType in TKS_INSTRUMENTS: 3490 for instrument in portfolio["stat"][iType]: 3491 if instrument["ticker"] == self.ticker: 3492 result = True 3493 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3494 break 3495 3496 elif self.figi: 3497 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3498 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3499 3500 for iType in TKS_INSTRUMENTS: 3501 for instrument in portfolio["stat"][iType]: 3502 if instrument["figi"] == self.figi: 3503 result = True 3504 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3505 break 3506 3507 else: 3508 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3509 3510 uLogger.debug(msg) 3511 3512 return result 3513 3514 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3515 """ 3516 Returns instrument is in the user's portfolio if it presents there. 3517 Instrument must be defined by `ticker` (highly priority) or `figi`. 3518 3519 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3520 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3521 """ 3522 result = None 3523 msg = "Instrument not defined!" 3524 3525 if portfolio is None or not portfolio: 3526 portfolio = self.Overview(show=False) 3527 3528 if self.ticker: 3529 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3530 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3531 3532 for iType in TKS_INSTRUMENTS: 3533 for instrument in portfolio["stat"][iType]: 3534 if instrument["ticker"] == self.ticker: 3535 result = instrument 3536 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3537 break 3538 3539 elif self.figi: 3540 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3541 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3542 3543 for iType in TKS_INSTRUMENTS: 3544 for instrument in portfolio["stat"][iType]: 3545 if instrument["figi"] == self.figi: 3546 result = instrument 3547 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3548 break 3549 3550 else: 3551 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3552 3553 uLogger.debug(msg) 3554 3555 return result 3556 3557 def RequestLimits(self) -> dict: 3558 """ 3559 Method for obtaining the available funds for withdrawal for current `accountId`. 3560 3561 See also: 3562 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3563 - `OverviewLimits()` method 3564 3565 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3566 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3567 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3568 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3569 """ 3570 if self.accountId is None or not self.accountId: 3571 uLogger.error("Variable `accountId` must be defined for using this method!") 3572 raise Exception("Account ID required") 3573 3574 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3575 3576 self.body = str({"accountId": self.accountId}) 3577 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3578 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3579 3580 uLogger.debug("Records about available funds for withdrawal successfully received") 3581 3582 return rawLimits 3583 3584 def OverviewLimits(self, show: bool = False) -> dict: 3585 """ 3586 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3587 3588 See also: `RequestLimits()`. 3589 3590 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3591 :return: dict with raw parsed data from server and some calculated statistics about it. 3592 """ 3593 if self.accountId is None or not self.accountId: 3594 uLogger.error("Variable `accountId` must be defined for using this method!") 3595 raise Exception("Account ID required") 3596 3597 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3598 3599 view = { 3600 "rawLimits": rawLimits, 3601 "limits": { # parsed data for every currency: 3602 "money": { # this is an array of portfolio currency positions 3603 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3604 }, 3605 "blocked": { # this is an array of blocked currency 3606 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3607 }, 3608 "blockedGuarantee": { # this is locked money under collateral for futures 3609 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3610 }, 3611 }, 3612 } 3613 3614 # --- Prepare text table with limits in human-readable format: 3615 if show: 3616 info = [ 3617 "# Withdrawal limits\n\n", 3618 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3619 "* **Account ID:** [{}]\n".format(self.accountId), 3620 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3621 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3622 ] 3623 3624 for curr in view["limits"]["money"].keys(): 3625 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3626 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3627 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3628 3629 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3630 "[{}]".format(curr), 3631 "{:.2f}".format(view["limits"]["money"][curr]), 3632 "{:.2f}".format(availableMoney), 3633 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3634 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3635 ) 3636 3637 if curr == "rub": 3638 info.insert(5, infoStr) # insert at first position in table and after headers 3639 3640 else: 3641 info.append(infoStr) 3642 3643 infoText = "".join(info) 3644 3645 uLogger.info(infoText) 3646 3647 if self.withdrawalLimitsFile: 3648 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3649 fH.write(infoText) 3650 3651 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3652 3653 return view 3654 3655 def RequestAccounts(self) -> dict: 3656 """ 3657 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3658 3659 See also: 3660 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3661 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3662 - `OverviewUserInfo()` method 3663 3664 :return: dict with raw data from server that contains accounts info. Example of dict: 3665 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3666 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3667 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3668 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3669 """ 3670 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3671 3672 self.body = str({}) 3673 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3674 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3675 3676 uLogger.debug("Records about available accounts successfully received") 3677 3678 return rawAccounts 3679 3680 def RequestUserInfo(self) -> dict: 3681 """ 3682 Method for requesting common user's information. 3683 3684 See also: 3685 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3686 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3687 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3688 - `OverviewUserInfo()` method 3689 3690 :return: dict with raw data from server that contains user's information. Example of dict: 3691 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3692 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3693 """ 3694 uLogger.debug("Requesting common user's information. Wait, please...") 3695 3696 self.body = str({}) 3697 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3698 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3699 3700 uLogger.debug("Records about current user successfully received") 3701 3702 return rawUserInfo 3703 3704 def RequestMarginStatus(self, accountId: str = None) -> dict: 3705 """ 3706 Method for requesting margin calculation for defined account ID. 3707 3708 See also: 3709 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3710 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3711 - `OverviewUserInfo()` method 3712 3713 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3714 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3715 Example of responses: 3716 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3717 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3718 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3719 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3720 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3721 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3722 """ 3723 if accountId is None or not accountId: 3724 if self.accountId is None or not self.accountId: 3725 uLogger.error("Variable `accountId` must be defined for using this method!") 3726 raise Exception("Account ID required") 3727 3728 else: 3729 accountId = self.accountId # use `self.accountId` (main ID) by default 3730 3731 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3732 3733 self.body = str({"accountId": accountId}) 3734 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3735 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3736 3737 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3738 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3739 rawMargin = {} 3740 3741 else: 3742 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3743 3744 return rawMargin 3745 3746 def RequestTariffLimits(self) -> dict: 3747 """ 3748 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3749 3750 See also: 3751 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3752 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3753 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3754 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3755 - `OverviewUserInfo()` method 3756 3757 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3758 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3759 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3760 """ 3761 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3762 3763 self.body = str({}) 3764 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3765 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3766 3767 uLogger.debug("Records with limits of current tariff successfully received") 3768 3769 return rawTariffLimits 3770 3771 def RequestBondCoupons(self, iJSON: dict) -> dict: 3772 """ 3773 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3774 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 3775 All dates are in UTC timezone. 3776 3777 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3778 Documentation: 3779 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3780 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3781 3782 See also: `ExtendBondsData()`. 3783 3784 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]` 3785 If raw iJSON is not data of bond then server returns an error [400] with message: 3786 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3787 :return: dictionary with bond payment calendar. Response example 3788 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3789 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3790 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3791 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3792 """ 3793 if iJSON["figi"] is None or not iJSON["figi"]: 3794 uLogger.error("FIGI must be defined for using this method!") 3795 raise Exception("FIGI required") 3796 3797 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 3798 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 3799 3800 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 3801 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 3802 self.figi, 3803 startDate, 3804 endDate, 3805 )) 3806 3807 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 3808 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 3809 calendar = self.SendAPIRequest(calendarURL, reqType="POST", debug=False) 3810 3811 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 3812 uLogger.warning("Instrument type is not bond!") 3813 3814 else: 3815 uLogger.debug("Records about bond payment calendar successfully received") 3816 3817 return calendar 3818 3819 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 3820 """ 3821 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 3822 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 3823 coupon yields, current yields and some statistics etc. 3824 3825 WARNING! This is too long operation if a lot of bonds requested from broker server. 3826 3827 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 3828 3829 :param instruments: list of strings with tickers or FIGIs. 3830 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 3831 for further used by data scientists or stock analytics. 3832 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 3833 In XLSX-file and Pandas DataFrame fields mean: 3834 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 3835 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 3836 """ 3837 if instruments is None or not instruments: 3838 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3839 raise Exception("Ticker or FIGI required") 3840 3841 if isinstance(instruments, str): 3842 instruments = [instruments] 3843 3844 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3845 3846 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 3847 3848 iCount = len(uniqueInstruments) 3849 tooLong = iCount >= 20 3850 if tooLong: 3851 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 3852 3853 bonds = None 3854 for i, self.figi in enumerate(uniqueInstruments): 3855 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 3856 3857 if "type" in instrument.keys() and instrument["type"] == "Bonds": 3858 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3859 rawBond = self.SearchByFIGI(requestPrice=True) 3860 3861 # Widen raw data with UTC current time (iData["actualDateTime"]): 3862 actualDate = datetime.now(tzutc()) 3863 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 3864 3865 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 3866 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 3867 3868 # Replace some values with human-readable: 3869 iData["nominalCurrency"] = iData["nominal"]["currency"] 3870 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 3871 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 3872 iData["aciCurrency"] = iData["aciValue"]["currency"] 3873 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 3874 iData["issueSize"] = int(iData["issueSize"]) 3875 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 3876 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 3877 iData["step"] = iData["step"] if "step" in iData.keys() else 0 3878 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 3879 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 3880 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 3881 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 3882 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 3883 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 3884 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 3885 3886 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 3887 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 3888 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 3889 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 3890 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 3891 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 3892 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 3893 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 3894 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 3895 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 3896 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 3897 3898 # Widen raw data with calendar data from `rawCalendar` values: 3899 calendarData = [] 3900 for item in iData["rawCalendar"]["events"]: 3901 calendarData.append({ 3902 "couponDate": item["couponDate"], 3903 "couponNumber": int(item["couponNumber"]), 3904 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 3905 "payCurrency": item["payOneBond"]["currency"], 3906 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 3907 "couponType": TKS_COUPON_TYPES[item["couponType"]], 3908 "couponStartDate": item["couponStartDate"], 3909 "couponEndDate": item["couponEndDate"], 3910 "couponPeriod": item["couponPeriod"], 3911 }) 3912 3913 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 3914 if "maturityDate" not in iData.keys(): 3915 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 3916 3917 # Widen raw data with Coupon Rate. 3918 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 3919 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 3920 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 3921 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 3922 3923 # Widen raw data with Yield to Maturity (YTM) on current date. 3924 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 3925 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 3926 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 3927 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 3928 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 3929 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 3930 3931 iData["calendar"] = calendarData # adds calendar at the end 3932 3933 # Remove not used data: 3934 iData.pop("uid") 3935 iData.pop("positionUid") 3936 iData.pop("currentPrice") 3937 iData.pop("rawCalendar") 3938 3939 colNames = list(iData.keys()) 3940 if bonds is None: 3941 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 3942 3943 else: 3944 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 3945 3946 else: 3947 uLogger.warning("Instrument with ticker [{}] and FIGI [{}] is not a bond!".format(instrument["ticker"], instrument["figi"])) 3948 3949 processed = round(100 * (i + 1) / iCount, 1) 3950 if tooLong and processed % 5 == 0: 3951 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 3952 3953 else: 3954 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 3955 3956 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 3957 3958 # Saving bonds from Pandas DataFrame to XLSX sheet: 3959 if xlsx and self.bondsXLSXFile: 3960 with pd.ExcelWriter( 3961 path=self.bondsXLSXFile, 3962 date_format=TKS_DATE_FORMAT, 3963 datetime_format=TKS_DATE_TIME_FORMAT, 3964 mode="w", 3965 ) as writer: 3966 bonds.to_excel( 3967 writer, 3968 sheet_name="Extended bonds data", 3969 index=True, 3970 encoding="UTF-8", 3971 freeze_panes=(1, 1), 3972 ) # saving as XLSX-file with freeze first row and column as headers 3973 3974 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 3975 3976 return bonds 3977 3978 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 3979 """ 3980 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 3981 3982 WARNING! This is too long operation if a lot of bonds requested from broker server. 3983 3984 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 3985 3986 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 3987 extended information about bonds: main info, current prices, bond payment calendar, 3988 coupon yields, current yields and some statistics etc. 3989 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 3990 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 3991 for further used by data scientists or stock analytics. 3992 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 3993 """ 3994 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 3995 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 3996 3997 uLogger.debug("Generating bond payments calendar data. Wait, please...") 3998 3999 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4000 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4001 calendar = None 4002 for bond in extBonds.iterrows(): 4003 for item in bond[1]["calendar"]: 4004 cData = { 4005 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4006 "couponDate": item["couponDate"], 4007 "figi": bond[1]["figi"], 4008 "ticker": bond[1]["ticker"], 4009 "name": bond[1]["name"], 4010 "couponNumber": item["couponNumber"], 4011 "payOneBond": item["payOneBond"], 4012 "payCurrency": item["payCurrency"], 4013 "couponType": item["couponType"], 4014 "couponPeriod": item["couponPeriod"], 4015 "fixDate": item["fixDate"], 4016 "couponStartDate": item["couponStartDate"], 4017 "couponEndDate": item["couponEndDate"], 4018 } 4019 4020 if calendar is None: 4021 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4022 4023 else: 4024 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4025 4026 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4027 4028 # Saving calendar from Pandas DataFrame to XLSX sheet: 4029 if xlsx: 4030 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4031 4032 with pd.ExcelWriter( 4033 path=xlsxCalendarFile, 4034 date_format=TKS_DATE_FORMAT, 4035 datetime_format=TKS_DATE_TIME_FORMAT, 4036 mode="w", 4037 ) as writer: 4038 humanReadable = calendar.copy(deep=True) 4039 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4040 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4041 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4042 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4043 humanReadable.columns = colNames # human-readable column names 4044 4045 humanReadable.to_excel( 4046 writer, 4047 sheet_name="Bond payments calendar", 4048 index=False, 4049 encoding="UTF-8", 4050 freeze_panes=(1, 2), 4051 ) # saving as XLSX-file with freeze first row and column as headers 4052 4053 del humanReadable # release df in memory 4054 4055 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4056 4057 return calendar 4058 4059 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4060 """ 4061 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4062 Also, creates Markdown file with calendar data, `calendar.md` by default. 4063 4064 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4065 4066 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4067 extended information about bonds: main info, current prices, bond payment calendar, 4068 coupon yields, current yields and some statistics etc. 4069 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4070 :param show: if `True` then also printing bonds payment calendar to the console, 4071 otherwise save to file `calendarFile` only. `False` by default. 4072 :return: multilines text in Markdown format with bonds payment calendar as a table. 4073 """ 4074 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4075 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4076 4077 infoText = "# Bond payments calendar\n\n" 4078 4079 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4080 4081 if not calendar.empty: 4082 splitLine = "| | | | | | | | | |\n" 4083 4084 info = [ 4085 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4086 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4087 ] 4088 4089 newMonth = False 4090 notOneBond = calendar["figi"].nunique() > 1 4091 for i, bond in enumerate(calendar.iterrows()): 4092 if newMonth and notOneBond: 4093 info.append(splitLine) 4094 4095 info.append( 4096 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4097 " √" if bond[1]["paid"] else " —", 4098 bond[1]["couponDate"].split("T")[0], 4099 bond[1]["figi"], 4100 bond[1]["ticker"], 4101 bond[1]["couponNumber"], 4102 "{} {}".format( 4103 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4104 bond[1]["payCurrency"], 4105 ), 4106 bond[1]["couponType"], 4107 bond[1]["couponPeriod"], 4108 bond[1]["fixDate"].split("T")[0], 4109 ) 4110 ) 4111 4112 if i < len(calendar.values) - 1: 4113 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4114 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4115 newMonth = False if curDate.month == nextDate.month else True 4116 4117 else: 4118 newMonth = False 4119 4120 infoText += "".join(info) 4121 4122 if show: 4123 uLogger.info("{}".format(infoText)) 4124 4125 if self.calendarFile is not None: 4126 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4127 fH.write(infoText) 4128 4129 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4130 4131 else: 4132 infoText += "No data\n" 4133 4134 return infoText 4135 4136 def OverviewAccounts(self, show: bool = False) -> dict: 4137 """ 4138 Method for parsing and show simple table with all available user accounts. 4139 4140 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4141 4142 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4143 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4144 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4145 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4146 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4147 "closed": "—", "access": "Full access" }, ...}}` 4148 """ 4149 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4150 4151 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4152 accounts = { 4153 item["id"]: { 4154 "type": TKS_ACCOUNT_TYPES[item["type"]], 4155 "name": item["name"], 4156 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4157 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4158 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4159 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4160 } for item in rawAccounts["accounts"] 4161 } 4162 4163 # Raw and parsed data with some fields replaced in "stat" section: 4164 view = { 4165 "rawAccounts": rawAccounts, 4166 "stat": accounts, 4167 } 4168 4169 # --- Prepare simple text table with only accounts data in human-readable format: 4170 if show: 4171 info = [ 4172 "# User accounts\n\n", 4173 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4174 "| Account ID | Type | Status | Name |\n", 4175 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4176 ] 4177 4178 for account in view["stat"].keys(): 4179 info.extend([ 4180 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4181 account, 4182 view["stat"][account]["type"], 4183 view["stat"][account]["status"], 4184 view["stat"][account]["name"], 4185 ) 4186 ]) 4187 4188 infoText = "".join(info) 4189 4190 uLogger.info(infoText) 4191 4192 if self.userAccountsFile: 4193 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4194 fH.write(infoText) 4195 4196 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4197 4198 return view 4199 4200 def OverviewUserInfo(self, show: bool = False) -> dict: 4201 """ 4202 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4203 4204 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4205 4206 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4207 :return: dict with raw parsed data from server and some calculated statistics about it. 4208 """ 4209 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4210 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4211 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4212 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4213 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4214 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4215 4216 # This is dict with parsed common user data: 4217 userInfo = { 4218 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4219 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4220 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4221 "tariff": rawUserInfo["tariff"], 4222 } 4223 4224 # This is an array of dict with parsed margin statuses for every account IDs: 4225 margins = {} 4226 for accountId in accounts.keys(): 4227 if rawMargins[accountId]: 4228 margins[accountId] = { 4229 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4230 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4231 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4232 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4233 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4234 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4235 } 4236 4237 else: 4238 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4239 4240 unary = {} # unary-connection limits 4241 for item in rawTariffLimits["unaryLimits"]: 4242 if item["limitPerMinute"] in unary.keys(): 4243 unary[item["limitPerMinute"]].extend(item["methods"]) 4244 4245 else: 4246 unary[item["limitPerMinute"]] = item["methods"] 4247 4248 stream = {} # stream-connection limits 4249 for item in rawTariffLimits["streamLimits"]: 4250 if item["limit"] in stream.keys(): 4251 stream[item["limit"]].extend(item["streams"]) 4252 4253 else: 4254 stream[item["limit"]] = item["streams"] 4255 4256 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4257 limits = { 4258 "unary": unary, 4259 "stream": stream, 4260 } 4261 4262 # Raw and parsed data as an output result: 4263 view = { 4264 "rawUserInfo": rawUserInfo, 4265 "rawAccounts": rawAccounts, 4266 "rawMargins": rawMargins, 4267 "rawTariffLimits": rawTariffLimits, 4268 "stat": { 4269 "userInfo": userInfo, 4270 "accounts": accounts, 4271 "margins": margins, 4272 "limits": limits, 4273 }, 4274 } 4275 4276 # --- Prepare text table with user information in human-readable format: 4277 if show: 4278 info = [ 4279 "# Full user information\n\n", 4280 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4281 "## Common information\n\n", 4282 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4283 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4284 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4285 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4286 "\n## User accounts\n\n", 4287 ] 4288 4289 for account in view["stat"]["accounts"].keys(): 4290 info.extend([ 4291 "### ID: [{}]\n\n".format(account), 4292 "| Parameters | Values |\n", 4293 "|----------------------|--------------------------------------------------------------|\n", 4294 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4295 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4296 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4297 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4298 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4299 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4300 ]) 4301 4302 if margins[account]: 4303 info.extend([ 4304 "| Margin status: | Enabled |\n", 4305 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4306 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4307 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4308 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4309 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4310 ]) 4311 4312 else: 4313 info.append("| Margin status: | Disabled |\n\n") 4314 4315 info.extend([ 4316 "\n## Current user tariff limits\n", 4317 "\nSee also:\n", 4318 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4319 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4320 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4321 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4322 "\n### Unary limits\n", 4323 ]) 4324 4325 if unary: 4326 for key, values in sorted(unary.items()): 4327 info.append("\n* Max requests per minute: {}\n".format(key)) 4328 4329 for value in values: 4330 info.append(" - {}\n".format(value)) 4331 4332 else: 4333 info.append("\nNot available\n") 4334 4335 info.append("\n### Stream limits\n") 4336 4337 if stream: 4338 for key, values in sorted(stream.items()): 4339 info.append("\n* Max stream connections: {}\n".format(key)) 4340 4341 for value in values: 4342 info.append(" - {}\n".format(value)) 4343 4344 else: 4345 info.append("\nNot available\n") 4346 4347 infoText = "".join(info) 4348 4349 uLogger.info(infoText) 4350 4351 if self.userInfoFile: 4352 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4353 fH.write(infoText) 4354 4355 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4356 4357 return view
This class implements methods to work with Tinkoff broker server.
Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
About token: https://tinkoff.github.io/investAPI/token/
198 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 199 """ 200 Main class init. 201 202 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 203 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 204 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 205 :param useCache: use default cache file with raw data to use instead of `iList`. 206 True by default. Cache is auto-update if new day has come. 207 If you don't want to use cache and always updates raw data then set `useCache=False`. 208 :param defaultCache: path to default cache file. `dump.json` by default. 209 """ 210 if token is None or not token: 211 try: 212 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 213 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 214 215 except KeyError: 216 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 217 raise Exception("Token required") 218 219 else: 220 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 221 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 222 223 if accountId is None or not accountId: 224 try: 225 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 226 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 227 228 except KeyError: 229 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 230 231 else: 232 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 233 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 234 235 self.version = __version__ # duplicate here used TKSBrokerAPI main version 236 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 237 238 Latest version: https://pypi.org/project/tksbrokerapi/ 239 """ 240 241 self.aliases = TKS_TICKER_ALIASES 242 """Some aliases instead official tickers. 243 244 See also: `TKSEnums.TKS_TICKER_ALIASES` 245 """ 246 247 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 248 249 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 250 251 self.ticker = "" 252 """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 253 254 See also: `SearchByTicker()`, `SearchInstruments()`. 255 """ 256 257 self.figi = "" 258 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. 259 260 See also: `SearchByFIGI()`, `SearchInstruments()`. 261 """ 262 263 self.depth = 1 264 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 265 266 See also: `GetCurrentPrices()`. 267 """ 268 269 self.server = r"https://invest-public-api.tinkoff.ru/rest" 270 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 271 272 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 273 """ 274 275 uLogger.debug("Broker API server: {}".format(self.server)) 276 277 self.timeout = 15 278 """Server operations timeout in seconds. Default: `15`. 279 280 See also: `SendAPIRequest()`. 281 """ 282 283 self.headers = { 284 "Content-Type": "application/json", 285 "accept": "application/json", 286 "Authorization": "Bearer {}".format(self.token), 287 "x-app-name": "Tim55667757.TKSBrokerAPI", 288 } 289 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 290 291 See also: `SendAPIRequest()`. 292 """ 293 294 self.body = None 295 """Request body which send to broker server. Default: `None`. 296 297 See also: `SendAPIRequest()`. 298 """ 299 300 self.historyFile = None 301 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 302 303 See also: `History()`. 304 """ 305 306 self.htmlHistoryFile = "index.html" 307 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 308 309 See also: `ShowHistoryChart()`. 310 """ 311 312 self.instrumentsFile = "instruments.md" 313 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 314 315 See also: `ShowInstrumentsInfo()`. 316 """ 317 318 self.searchResultsFile = "search-results.md" 319 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 320 321 See also: `SearchInstruments()`. 322 """ 323 324 self.pricesFile = "prices.md" 325 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 326 327 See also: `GetListOfPrices()`. 328 """ 329 330 self.infoFile = "info.md" 331 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 332 333 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 334 """ 335 336 self.bondsXLSXFile = "ext-bonds.xlsx" 337 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 338 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 339 340 See also: `ExtendBondsData()`. 341 """ 342 343 self.calendarFile = "calendar.md" 344 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 345 346 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 347 348 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 349 """ 350 351 self.overviewFile = "overview.md" 352 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 353 354 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 355 """ 356 357 self.overviewDigestFile = "overview-digest.md" 358 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 359 360 See also: `Overview()` with parameter `details="digest"`. 361 """ 362 363 self.overviewPositionsFile = "overview-positions.md" 364 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 365 366 See also: `Overview()` with parameter `details="positions"`. 367 """ 368 369 self.overviewOrdersFile = "overview-orders.md" 370 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 371 372 See also: `Overview()` with parameter `details="orders"`. 373 """ 374 375 self.overviewAnalyticsFile = "overview-analytics.md" 376 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 377 378 See also: `Overview()` with parameter `details="analytics"`. 379 """ 380 381 self.reportFile = "deals.md" 382 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 383 384 See also: `Deals()`. 385 """ 386 387 self.withdrawalLimitsFile = "limits.md" 388 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 389 390 See also: `OverviewLimits()` and `RequestLimits()`. 391 """ 392 393 self.userInfoFile = "user-info.md" 394 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 395 396 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 397 """ 398 399 self.userAccountsFile = "accounts.md" 400 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 401 402 See also: `OverviewAccounts()`, `RequestAccounts()`. 403 """ 404 405 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 406 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 407 408 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 409 410 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 411 """ 412 413 self.iList = None # init iList for raw instruments data 414 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 415 416 See also: `Listing()`, `DumpInstruments()`. 417 """ 418 419 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 420 if useCache: 421 if os.path.exists(self.iListDumpFile): 422 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 423 curTime = datetime.now(tzutc()) 424 425 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 426 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 427 428 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 429 430 else: 431 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 432 433 uLogger.debug("Local cache with raw instruments data is used: [{}]".format(os.path.abspath(self.iListDumpFile))) 434 uLogger.debug("Dump file was last modified [{}] UTC".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 435 436 else: 437 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 438 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 439 440 else: 441 self.iList = self.Listing() # request new raw instruments data from broker server 442 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 443 444 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 445 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 446 447 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 448 """
Main class init.
Parameters
- token: Bearer token for Tinkoff Invest API. It can be set from environment variable
TKS_API_TOKEN. - accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
Also, this variable can be set from environment variable
TKS_ACCOUNT_ID. - useCache: use default cache file with raw data to use instead of
iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then setuseCache=False. - defaultCache: path to default cache file.
dump.jsonby default.
Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
Latest version: https://pypi.org/project/tksbrokerapi/
String with ticker, e.g. GOOGL. Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc. More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.
See also: SearchByTicker(), SearchInstruments().
String with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6.
See also: SearchByFIGI(), SearchInstruments().
Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.
See also: GetCurrentPrices().
Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().
Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.
See also: SendAPIRequest().
Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.
See also: History().
Full path to the html file where rendered candles chart stored. Default: index.html.
See also: ShowHistoryChart().
Filename where full available to user instruments list will be saved. Default: instruments.md.
See also: ShowInstrumentsInfo().
Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.
See also: SearchInstruments().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: GetListOfPrices().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().
Filename where wider Pandas DataFrame with more information about bonds: main info, current prices,
bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.
See also: ExtendBondsData().
Filename where bonds payment calendar will be saved. Default: calendar.md.
Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.
See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().
Filename where current portfolio, open trades and orders will be saved. Default: overview.md.
See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().
Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.
See also: Overview() with parameter details="digest".
Filename where only open positions, without everything else will be saved. Default: overview-positions.md.
See also: Overview() with parameter details="positions".
Filename where open limits and stop orders will be saved. Default: overview-orders.md.
See also: Overview() with parameter details="orders".
Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.
See also: Overview() with parameter details="analytics".
Filename where history of deals and trade statistics will be saved. Default: deals.md.
See also: Deals().
Filename where table of funds available for withdrawal will be saved. Default: limits.md.
See also: OverviewLimits() and RequestLimits().
Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.
See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().
Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.
See also: OverviewAccounts(), RequestAccounts().
Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.
Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.
See also: DumpInstruments() and DumpInstrumentsAsXLSX().
Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.
See also: Listing(), DumpInstruments().
PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
472 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5, debug: bool = False) -> dict: 473 """ 474 Send GET or POST request to broker server and receive JSON object. 475 476 self.header: must be defining with dictionary of headers. 477 self.body: if define then used as request body. None by default. 478 self.timeout: global request timeout, 15 seconds by default. 479 :param url: url with REST request. 480 :param reqType: send "GET" or "POST" request. "GET" by default. 481 :param retry: how many times retry after first request if an 5xx server errors occurred. 482 :param pause: sleep time in seconds between retries. 483 :param debug: if `True` then print more debug information, e.g. request and response parameters, headers etc. 484 :return: response JSON (dictionary) from broker. 485 """ 486 if reqType not in ("GET", "POST"): 487 uLogger.error("You can define request type: 'GET' or 'POST'!") 488 raise Exception("Incorrect value") 489 490 if debug: 491 uLogger.debug("Request parameters:") 492 uLogger.debug(" - REST API URL: {}".format(url)) 493 uLogger.debug(" - request type: {}".format(reqType)) 494 uLogger.debug(" - headers: {}".format(str(self.headers).replace(self.token, "*** request token ***"))) 495 uLogger.debug(" - body: {}".format(self.body)) 496 497 # fast hack to avoid all operations with some tickers/FIGI 498 responseJSON = {} 499 oK = True 500 for item in self.exclude: 501 if item in url: 502 if debug: 503 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 504 505 oK = False 506 break 507 508 if oK: 509 counter = 0 510 response = None 511 errMsg = "" 512 513 while not response and counter <= retry: 514 if reqType == "GET": 515 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 516 517 if reqType == "POST": 518 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 519 520 if debug: 521 uLogger.debug("Response:") 522 uLogger.debug(" - status code: {}".format(response.status_code)) 523 uLogger.debug(" - reason: {}".format(response.reason)) 524 uLogger.debug(" - body length: {}".format(len(response.text))) 525 uLogger.debug(" - headers: {}".format(response.headers)) 526 527 # Server returns some headers: 528 # - `x-ratelimit-limit` - shows the settings of the current user limit for this method. 529 # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute. 530 # - `x-ratelimit-reset` - time in seconds before resetting the request counter. 531 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 532 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 533 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 534 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 535 sleep(rateLimitWait) 536 537 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 538 if 400 <= response.status_code < 500: 539 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 540 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 541 counter = retry + 1 542 543 if 500 <= response.status_code < 600: 544 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 545 uLogger.debug(" - not oK, {}".format(errMsg)) 546 counter += 1 547 548 if counter <= retry: 549 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 550 sleep(pause) 551 552 responseJSON = self._ParseJSON(response.text) 553 554 if errMsg: 555 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 556 uLogger.error(" - not oK, {}".format(errMsg)) 557 558 return responseJSON
Send GET or POST request to broker server and receive JSON object.
self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.
Parameters
- url: url with REST request.
- reqType: send "GET" or "POST" request. "GET" by default.
- retry: how many times retry after first request if an 5xx server errors occurred.
- pause: sleep time in seconds between retries.
- debug: if
Truethen print more debug information, e.g. request and response parameters, headers etc.
Returns
response JSON (dictionary) from broker.
591 def Listing(self) -> dict: 592 """ 593 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 594 595 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 596 """ 597 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 598 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 599 600 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 601 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 602 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 603 604 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 605 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 606 poolUpdater.close() 607 608 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 609 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 610 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 611 612 # calculate minimum price increment (step) for all instruments and set up instrument's type: 613 for iType in iList.keys(): 614 for ticker in iList[iType]: 615 iList[iType][ticker]["type"] = iType 616 617 if "minPriceIncrement" in iList[iType][ticker].keys(): 618 iList[iType][ticker]["step"] = NanoToFloat( 619 iList[iType][ticker]["minPriceIncrement"]["units"], 620 iList[iType][ticker]["minPriceIncrement"]["nano"], 621 ) 622 623 else: 624 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 625 626 return iList
Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
Returns
Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
628 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 629 """ 630 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 631 632 See also: `DumpInstruments()`, `Listing()`. 633 634 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 635 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 636 """ 637 if self.iListDumpFile is None or not self.iListDumpFile: 638 uLogger.error("Output name of dump file must be defined!") 639 raise Exception("Filename required") 640 641 if not self.iList or forceUpdate: 642 self.iList = self.Listing() 643 644 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 645 646 # Save as XLSX with separated sheets for every type of instruments: 647 with pd.ExcelWriter( 648 path=xlsxDumpFile, 649 date_format=TKS_DATE_FORMAT, 650 datetime_format=TKS_DATE_TIME_FORMAT, 651 mode="w", 652 ) as writer: 653 for iType in TKS_INSTRUMENTS: 654 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 655 df = df[sorted(df)] # sorted by column names 656 df = df.applymap( 657 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 658 na_action="ignore", 659 ) # converting numbers from nano-type to float in every cell 660 df.to_excel( 661 writer, 662 sheet_name=iType, 663 encoding="UTF-8", 664 freeze_panes=(1, 1), 665 ) # saving as XLSX-file with freeze first row and column as headers 666 667 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
See also: DumpInstruments(), Listing().
Parameters
669 def DumpInstruments(self, forceUpdate: bool = True) -> str: 670 """ 671 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 672 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 673 674 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 675 676 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 677 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 678 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 679 """ 680 if self.iListDumpFile is None or not self.iListDumpFile: 681 uLogger.error("Output name of dump file must be defined!") 682 raise Exception("Filename required") 683 684 if not self.iList or forceUpdate: 685 self.iList = self.Listing() 686 687 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 688 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 689 fH.write(jsonDump) 690 691 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 692 693 return jsonDump
Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
using Listing() method. If iListDumpFile string is not empty then also save information to this file.
See also: DumpInstrumentsAsXLSX(), Listing().
Parameters
- forceUpdate: if
Truethen at first updates data withListing()method, otherwise just saves existiListas JSON-file (default:dump.json).
Returns
serialized JSON formatted
strwith full data of instruments, also saved to the--outputJSON-file.
695 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 696 """ 697 Show information about one instrument defined by json data and prints it in Markdown format. 698 699 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 700 701 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 702 :param show: if `True` then also printing information about instrument and its current price. 703 :return: multilines text in Markdown format with information about one instrument. 704 """ 705 splitLine = "| | |\n" 706 infoText = "" 707 708 if iJSON is not None and iJSON and isinstance(iJSON, dict): 709 info = [ 710 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 711 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 712 "| Parameters | Values |\n", 713 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 714 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 715 "| Full name: | {:<54} |\n".format(iJSON["name"]), 716 ] 717 718 if "sector" in iJSON.keys() and iJSON["sector"]: 719 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 720 721 info.append("| Country of instrument: | {:<54} |\n".format("{}{}".format( 722 "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "", 723 iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "", 724 ))) 725 726 info.extend([ 727 splitLine, 728 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 729 "| Exchange: | {:<54} |\n".format(iJSON["exchange"]), 730 ]) 731 732 if "isin" in iJSON.keys() and iJSON["isin"]: 733 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 734 735 if "classCode" in iJSON.keys(): 736 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 737 738 info.extend([ 739 splitLine, 740 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 741 splitLine, 742 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 743 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 744 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 745 ]) 746 747 if iJSON["figi"]: 748 self.figi = iJSON["figi"] 749 iJSON = iJSON | self.RequestTradingStatus() 750 751 info.extend([ 752 splitLine, 753 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 754 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 755 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 756 ]) 757 758 info.append(splitLine) 759 760 if "type" in iJSON.keys() and iJSON["type"]: 761 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 762 763 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 764 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 765 766 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 767 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 768 769 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 770 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 771 772 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 773 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 774 775 if "focusType" in iJSON.keys() and iJSON["focusType"]: 776 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 777 778 if "assetType" in iJSON.keys() and iJSON["assetType"]: 779 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 780 781 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 782 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 783 784 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 785 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 786 787 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 788 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 789 790 if "currency" in iJSON.keys(): 791 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 792 793 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 794 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 795 796 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 797 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 798 799 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 800 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 801 802 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 803 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 804 805 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 806 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 807 808 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 809 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 810 811 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 812 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 813 814 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 815 info.append("| Perpetual bond: | Yes |\n") 816 817 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 818 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 819 820 iExt = None 821 if iJSON["type"] == "Bonds": 822 info.extend([ 823 splitLine, 824 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 825 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 826 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 827 iJSON["nominal"]["currency"], 828 )), 829 ]) 830 831 if "floatingCouponFlag" in iJSON.keys(): 832 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 833 834 if "amortizationFlag" in iJSON.keys(): 835 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 836 837 info.append(splitLine) 838 839 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 840 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 841 842 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 843 844 info.extend([ 845 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 846 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 847 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 848 ]) 849 850 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 851 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 852 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 853 iJSON["aciValue"]["currency"] 854 ))) 855 856 if "currentPrice" in iJSON.keys(): 857 info.append(splitLine) 858 859 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 860 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 861 862 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 863 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 864 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 865 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 866 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 867 868 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 869 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 870 871 info.extend([ 872 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 873 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 874 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 875 )), 876 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 877 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 878 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 879 )), 880 "| Changes between last deal price and last close | {:<54} |\n".format( 881 "{:.2f}%{}".format( 882 iJSON["currentPrice"]["changes"], 883 " ({}{:.2f} {})".format( 884 "+" if bondChangesDelta > 0 else "", 885 bondChangesDelta, 886 aciCurrency 887 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 888 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 889 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 890 currency 891 ), 892 ) 893 ), 894 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 895 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 896 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 897 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 898 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 899 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 900 )), 901 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 902 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 903 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 904 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 905 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 906 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 907 )), 908 ]) 909 910 if "lot" in iJSON.keys(): 911 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 912 913 if "step" in iJSON.keys() and iJSON["step"] != 0: 914 info.append("| Minimum price increment (step): | {:<54} |\n".format(iJSON["step"])) 915 916 # Add bond payment calendar: 917 if iJSON["type"] == "Bonds": 918 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 919 info.extend(["\n", strCalendar]) 920 921 infoText += "".join(info) 922 923 if show: 924 uLogger.info("{}".format(infoText)) 925 926 else: 927 uLogger.debug("{}".format(infoText)) 928 929 if self.infoFile is not None: 930 with open(self.infoFile, "w", encoding="UTF-8") as fH: 931 fH.write(infoText) 932 933 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 934 935 return infoText
Show information about one instrument defined by json data and prints it in Markdown format.
See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().
Parameters
- iJSON: json data of instrument, example:
iJSON = self.iList["Shares"][self.ticker] - show: if
Truethen also printing information about instrument and its current price.
Returns
multilines text in Markdown format with information about one instrument.
937 def SearchByTicker(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict: 938 """ 939 Search and return raw broker's information about instrument by its ticker. 940 `ticker` must be defined! If debug=True then print all debug messages. 941 942 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 943 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 944 :param debug: if `True` then print all debug console messages. 945 :return: JSON formatted data with information about instrument. 946 """ 947 tickerJSON = {} 948 if debug: 949 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 950 951 if not self.ticker: 952 uLogger.warning("self.ticker variable is not be empty!") 953 954 else: 955 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 956 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 957 raise Exception("Instrument not allowed") 958 959 if not self.iList: 960 self.iList = self.Listing() 961 962 if self.ticker in self.iList["Shares"].keys(): 963 tickerJSON = self.iList["Shares"][self.ticker] 964 if debug: 965 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 966 967 elif self.ticker in self.iList["Currencies"].keys(): 968 tickerJSON = self.iList["Currencies"][self.ticker] 969 if debug: 970 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 971 972 elif self.ticker in self.iList["Bonds"].keys(): 973 tickerJSON = self.iList["Bonds"][self.ticker] 974 if debug: 975 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 976 977 elif self.ticker in self.iList["Etfs"].keys(): 978 tickerJSON = self.iList["Etfs"][self.ticker] 979 if debug: 980 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 981 982 elif self.ticker in self.iList["Futures"].keys(): 983 tickerJSON = self.iList["Futures"][self.ticker] 984 if debug: 985 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 986 987 if tickerJSON: 988 self.figi = tickerJSON["figi"] 989 990 if requestPrice: 991 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 992 993 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 994 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 995 996 else: 997 tickerJSON["currentPrice"]["changes"] = 0 998 999 if show: 1000 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 1001 1002 else: 1003 if show: 1004 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 1005 1006 return tickerJSON
Search and return raw broker's information about instrument by its ticker.
ticker must be defined! If debug=True then print all debug messages.
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (because this is long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console. - debug: if
Truethen print all debug console messages.
Returns
JSON formatted data with information about instrument.
1008 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict: 1009 """ 1010 Search and return raw broker's information about instrument by its FIGI. 1011 `figi` must be defined! If debug=True then print all debug messages. 1012 1013 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1014 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1015 :param debug: if `True` then print all debug console messages. 1016 :return: JSON formatted data with information about instrument. 1017 """ 1018 figiJSON = {} 1019 if debug: 1020 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 1021 1022 if not self.figi: 1023 uLogger.warning("self.figi variable is not be empty!") 1024 1025 else: 1026 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1027 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 1028 raise Exception("Instrument not allowed") 1029 1030 if not self.iList: 1031 self.iList = self.Listing() 1032 1033 for item in self.iList["Shares"].keys(): 1034 if self.figi == self.iList["Shares"][item]["figi"]: 1035 figiJSON = self.iList["Shares"][item] 1036 1037 if debug: 1038 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 1039 1040 break 1041 1042 if not figiJSON: 1043 for item in self.iList["Currencies"].keys(): 1044 if self.figi == self.iList["Currencies"][item]["figi"]: 1045 figiJSON = self.iList["Currencies"][item] 1046 1047 if debug: 1048 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 1049 1050 break 1051 1052 if not figiJSON: 1053 for item in self.iList["Bonds"].keys(): 1054 if self.figi == self.iList["Bonds"][item]["figi"]: 1055 figiJSON = self.iList["Bonds"][item] 1056 1057 if debug: 1058 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 1059 1060 break 1061 1062 if not figiJSON: 1063 for item in self.iList["Etfs"].keys(): 1064 if self.figi == self.iList["Etfs"][item]["figi"]: 1065 figiJSON = self.iList["Etfs"][item] 1066 1067 if debug: 1068 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 1069 1070 break 1071 1072 if not figiJSON: 1073 for item in self.iList["Futures"].keys(): 1074 if self.figi == self.iList["Futures"][item]["figi"]: 1075 figiJSON = self.iList["Futures"][item] 1076 1077 if debug: 1078 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 1079 1080 break 1081 1082 if figiJSON: 1083 self.figi = figiJSON["figi"] 1084 self.ticker = figiJSON["ticker"] 1085 1086 if requestPrice: 1087 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1088 1089 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1090 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1091 1092 else: 1093 figiJSON["currentPrice"]["changes"] = 0 1094 1095 if show: 1096 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1097 1098 else: 1099 if show: 1100 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 1101 1102 return figiJSON
Search and return raw broker's information about instrument by its FIGI.
figi must be defined! If debug=True then print all debug messages.
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (it's long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console. - debug: if
Truethen print all debug console messages.
Returns
JSON formatted data with information about instrument.
1104 def GetCurrentPrices(self, show: bool = True) -> dict: 1105 """ 1106 Get and show Depth of Market with current prices of the instrument. If an error occurred then returns an empty record: 1107 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1108 1109 See also: 1110 1111 :param show: if `True` then print DOM to log and console. 1112 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1113 """ 1114 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1115 1116 if self.depth < 1: 1117 uLogger.error("Depth of Market (DOM) must be >=1!") 1118 raise Exception("Incorrect value") 1119 1120 if not (self.ticker or self.figi): 1121 uLogger.error("self.ticker or self.figi variables must be defined!") 1122 raise Exception("Ticker or FIGI required") 1123 1124 if self.ticker and not self.figi: 1125 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1126 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1127 1128 if not self.ticker and self.figi: 1129 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1130 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1131 1132 if not self.figi: 1133 uLogger.error("FIGI is not defined!") 1134 raise Exception("Ticker or FIGI required") 1135 1136 else: 1137 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1138 1139 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1140 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1141 self.body = str({"figi": self.figi, "depth": self.depth}) 1142 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") 1143 1144 if pricesResponse: 1145 # list of dicts with sellers orders: 1146 prices["buy"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1147 1148 # list of dicts with buyers orders: 1149 prices["sell"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1150 1151 # max price of instrument at this time: 1152 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1153 1154 # min price of instrument at this time: 1155 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1156 1157 # last price of deal with instrument: 1158 prices["lastPrice"] = NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]) if "lastPrice" in pricesResponse.keys() else 0 1159 1160 # last close price of instrument: 1161 prices["closePrice"] = NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]) if "closePrice" in pricesResponse.keys() else 0 1162 1163 else: 1164 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1165 uLogger.debug("Server response: {}".format(pricesResponse)) 1166 1167 if show: 1168 if prices["buy"] or prices["sell"]: 1169 info = [ 1170 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1171 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1172 self.ticker, 1173 self.figi, 1174 self.depth, 1175 ), 1176 uLog.sepShort, "\n", 1177 " Orders of Buyers | Orders of Sellers\n", 1178 uLog.sepShort, "\n", 1179 " Sell prices (vol.) | Buy prices (vol.)\n", 1180 uLog.sepShort, "\n", 1181 ] 1182 1183 if not prices["buy"]: 1184 info.append(" | No orders!\n") 1185 sumBuy = 0 1186 1187 else: 1188 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1189 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1190 for item in maxMinSorted: 1191 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1192 1193 if not prices["sell"]: 1194 info.append("No orders! |\n") 1195 sumSell = 0 1196 1197 else: 1198 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1199 for item in prices["sell"]: 1200 info.append("{:>19} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1201 1202 info.extend([ 1203 uLog.sepShort, "\n", 1204 "{:>19} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1205 uLog.sepShort, "\n", 1206 ]) 1207 1208 infoText = "".join(info) 1209 1210 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1211 1212 else: 1213 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1214 1215 return prices
Get and show Depth of Market with current prices of the instrument. If an error occurred then returns an empty record:
{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.
See also:
Parameters
- show: if
Truethen print DOM to log and console.
Returns
orders book dict with lists of current buy and sell prices:
{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}.
1217 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1218 """ 1219 This method get and show information about all available broker instruments for current user account. 1220 If `instrumentsFile` string is not empty then also save information to this file. 1221 1222 :param show: if `True` then print results to console, if `False` - print only to file. 1223 :return: multi-lines string with all available broker instruments 1224 """ 1225 if not self.iList: 1226 self.iList = self.Listing() 1227 1228 info = [ 1229 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1230 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1231 ] 1232 1233 # add instruments count by type: 1234 for iType in self.iList.keys(): 1235 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1236 1237 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1238 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1239 1240 # generating info tables with all instruments by type: 1241 for iType in self.iList.keys(): 1242 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1243 1244 for instrument in self.iList[iType].keys(): 1245 iName = self.iList[iType][instrument]["name"] # instrument's name 1246 if len(iName) > 57: 1247 iName = "{}...".format(iName[:54]) # right trim for a long string 1248 1249 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1250 self.iList[iType][instrument]["ticker"], 1251 iName, 1252 self.iList[iType][instrument]["figi"], 1253 self.iList[iType][instrument]["currency"], 1254 self.iList[iType][instrument]["lot"], 1255 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1256 )) 1257 1258 infoText = "".join(info) 1259 1260 if show: 1261 uLogger.info(infoText) 1262 1263 if self.instrumentsFile: 1264 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1265 fH.write(infoText) 1266 1267 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1268 1269 return infoText
This method get and show information about all available broker instruments for current user account.
If instrumentsFile string is not empty then also save information to this file.
Parameters
- show: if
Truethen print results to console, ifFalse- print only to file.
Returns
multi-lines string with all available broker instruments
1271 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1272 """ 1273 This method search and show information about instruments by part of its ticker, FIGI or name. 1274 If `searchResultsFile` string is not empty then also save information to this file. 1275 1276 :param pattern: string with part of ticker, FIGI or instrument's name. 1277 :param show: if `True` then print results to console, if `False` - return list of result only. 1278 :return: list of dictionaries with all found instruments. 1279 """ 1280 if not self.iList: 1281 self.iList = self.Listing() 1282 1283 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1284 compiledPattern = re.compile(pattern, re.IGNORECASE) 1285 1286 for iType in self.iList: 1287 for instrument in self.iList[iType].values(): 1288 searchResult = compiledPattern.search(" ".join( 1289 [instrument["ticker"], instrument["figi"], instrument["name"]] 1290 )) 1291 1292 if searchResult: 1293 searchResults[iType][instrument["ticker"]] = instrument 1294 1295 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1296 info = [ 1297 "# Search results\n\n", 1298 "* **Search pattern:** [{}]\n".format(pattern), 1299 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1300 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1301 ] 1302 infoShort = info[:] 1303 1304 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1305 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1306 skippedLine = "| ... | ... | ... | ... |\n" 1307 1308 if resultsLen == 0: 1309 info.append("\nNo results\n") 1310 infoShort.append("\nNo results\n") 1311 uLogger.warning("No results. Try changing your search pattern.") 1312 1313 else: 1314 for iType in searchResults: 1315 iTypeValuesCount = len(searchResults[iType].values()) 1316 if iTypeValuesCount > 0: 1317 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1318 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1319 1320 for instrument in searchResults[iType].values(): 1321 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1322 instrument["type"], 1323 instrument["ticker"], 1324 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1325 instrument["figi"], 1326 )) 1327 1328 if iTypeValuesCount <= 5: 1329 infoShort.extend(info[-iTypeValuesCount:]) 1330 1331 else: 1332 infoShort.extend(info[-5:]) 1333 infoShort.append(skippedLine) 1334 1335 infoText = "".join(info) 1336 infoTextShort = "".join(infoShort) 1337 1338 if show: 1339 uLogger.info(infoTextShort) 1340 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1341 1342 if self.searchResultsFile: 1343 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1344 fH.write(infoText) 1345 1346 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1347 1348 return searchResults
This method search and show information about instruments by part of its ticker, FIGI or name.
If searchResultsFile string is not empty then also save information to this file.
Parameters
- pattern: string with part of ticker, FIGI or instrument's name.
- show: if
Truethen print results to console, ifFalse- return list of result only.
Returns
list of dictionaries with all found instruments.
1350 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1351 """ 1352 Creating list with unique instrument FIGIs from input list of tickers or FIGIs. 1353 1354 :param instruments: list of strings with tickers or FIGIs. 1355 :return: list with unique instrument FIGIs only. 1356 """ 1357 requestedInstruments = [] 1358 for iName in instruments: 1359 if iName not in self.aliases.keys(): 1360 if iName not in requestedInstruments: 1361 requestedInstruments.append(iName) 1362 1363 else: 1364 if iName not in requestedInstruments: 1365 if self.aliases[iName] not in requestedInstruments: 1366 requestedInstruments.append(self.aliases[iName]) 1367 1368 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1369 1370 onlyUniqueFIGIs = [] 1371 for iName in requestedInstruments: 1372 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1373 continue 1374 1375 self.ticker = iName 1376 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1377 1378 if not iData: 1379 self.ticker = "" 1380 self.figi = iName 1381 1382 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1383 1384 if not iData: 1385 self.figi = "" 1386 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1387 1388 if iData and iData["figi"] not in onlyUniqueFIGIs: 1389 onlyUniqueFIGIs.append(iData["figi"]) 1390 1391 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1392 1393 return onlyUniqueFIGIs
Creating list with unique instrument FIGIs from input list of tickers or FIGIs.
Parameters
- instruments: list of strings with tickers or FIGIs.
Returns
list with unique instrument FIGIs only.
1395 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1396 """ 1397 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1398 See limits: https://tinkoff.github.io/investAPI/limits/ 1399 If `pricesFile` string is not empty then also save information to this file. 1400 1401 :param instruments: list of strings with tickers or FIGIs. 1402 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1403 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1404 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1405 """ 1406 if instruments is None or not instruments: 1407 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1408 raise Exception("Ticker or FIGI required") 1409 1410 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1411 1412 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1413 1414 iList = [] # trying to get info and current prices about all unique instruments: 1415 for self.figi in onlyUniqueFIGIs: 1416 iData = self.SearchByFIGI(requestPrice=True) 1417 iList.append(iData) 1418 1419 self.ShowListOfPrices(iList, show) 1420 1421 return iList
This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
See limits: https://tinkoff.github.io/investAPI/limits/
If pricesFile string is not empty then also save information to this file.
Parameters
- instruments: list of strings with tickers or FIGIs.
- show: if
Truethen prints prices to console, ifFalse- prints only to filepricesFile.
Returns
list of instruments looks like
[{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker()orSearchByFIGI()methods.
1423 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1424 """ 1425 Show table contains current prices of given instruments. 1426 1427 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1428 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1429 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1430 :return: multilines text in Markdown format as a table contains current prices. 1431 """ 1432 infoText = "" 1433 1434 if show or self.pricesFile: 1435 info = [ 1436 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1437 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1438 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1439 ] 1440 1441 for item in iList: 1442 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1443 item["ticker"], 1444 item["figi"], 1445 item["type"], 1446 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1447 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1448 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1449 "{} / {}".format( 1450 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1451 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1452 ), 1453 "{} / {}".format( 1454 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1455 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1456 ), 1457 item["currency"], 1458 )) 1459 1460 infoText = "".join(info) 1461 1462 if show: 1463 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1464 1465 if self.pricesFile: 1466 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1467 fH.write(infoText) 1468 1469 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1470 1471 return infoText
Show table contains current prices of given instruments.
Parameters
- **iList: list of instruments looks like
[{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker(requestPrice=True)or bySearchByFIGI(requestPrice=True)methods. - show: if
Truethen prints prices to console, ifFalse- prints only to filepricesFile.
Returns
multilines text in Markdown format as a table contains current prices.
1473 def RequestTradingStatus(self) -> dict: 1474 """ 1475 Requesting trading status for the instrument defined by `figi` variable. 1476 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1477 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1478 1479 :return: dictionary with trading status attributes. Response example: 1480 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1481 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1482 """ 1483 if self.figi is None or not self.figi: 1484 uLogger.error("Variable `figi` must be defined for using this method!") 1485 raise Exception("FIGI required") 1486 1487 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1488 1489 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1490 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1491 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1492 1493 uLogger.debug("Records about current trading status successfully received") 1494 1495 return tradingStatus
Requesting trading status for the instrument defined by figi variable.
REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
Returns
dictionary with trading status attributes. Response example:
{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}
1497 def RequestPortfolio(self) -> dict: 1498 """ 1499 Requesting actual user's portfolio for current `accountId`. 1500 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1501 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1502 1503 :return: dictionary with user's portfolio. 1504 """ 1505 if self.accountId is None or not self.accountId: 1506 uLogger.error("Variable `accountId` must be defined for using this method!") 1507 raise Exception("Account ID required") 1508 1509 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1510 1511 self.body = str({"accountId": self.accountId}) 1512 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1513 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1514 1515 uLogger.debug("Records about user's portfolio successfully received") 1516 1517 return rawPortfolio
Requesting actual user's portfolio for current accountId.
REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
Returns
dictionary with user's portfolio.
1519 def RequestPositions(self) -> dict: 1520 """ 1521 Requesting open positions by currencies and instruments for current `accountId`. 1522 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1523 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1524 1525 :return: dictionary with open positions by instruments. 1526 """ 1527 if self.accountId is None or not self.accountId: 1528 uLogger.error("Variable `accountId` must be defined for using this method!") 1529 raise Exception("Account ID required") 1530 1531 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1532 1533 self.body = str({"accountId": self.accountId}) 1534 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1535 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1536 1537 uLogger.debug("Records about current open positions successfully received") 1538 1539 return rawPositions
Requesting open positions by currencies and instruments for current accountId.
REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
Returns
dictionary with open positions by instruments.
1541 def RequestPendingOrders(self) -> list: 1542 """ 1543 Requesting current actual pending orders for current `accountId`. 1544 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1545 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1546 1547 :return: list of dictionaries with pending orders. 1548 """ 1549 if self.accountId is None or not self.accountId: 1550 uLogger.error("Variable `accountId` must be defined for using this method!") 1551 raise Exception("Account ID required") 1552 1553 uLogger.debug("Requesting current actual pending orders. Wait, please...") 1554 1555 self.body = str({"accountId": self.accountId}) 1556 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1557 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1558 1559 uLogger.debug("[{}] records about pending orders received".format(len(rawOrders))) 1560 1561 return rawOrders
Requesting current actual pending orders for current accountId.
REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
Returns
list of dictionaries with pending orders.
1563 def RequestStopOrders(self) -> list: 1564 """ 1565 Requesting current actual stop orders for current `accountId`. 1566 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1567 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1568 1569 :return: list of dictionaries with stop orders. 1570 """ 1571 if self.accountId is None or not self.accountId: 1572 uLogger.error("Variable `accountId` must be defined for using this method!") 1573 raise Exception("Account ID required") 1574 1575 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1576 1577 self.body = str({"accountId": self.accountId}) 1578 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1579 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1580 1581 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1582 1583 return rawStopOrders
Requesting current actual stop orders for current accountId.
REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
Returns
list of dictionaries with stop orders.
1585 def Overview(self, show: bool = False, details: str = "full") -> dict: 1586 """ 1587 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1588 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1589 are defined then also save information to file. 1590 1591 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1592 many requests about the state of the portfolio, and then, based on the received data, a large number 1593 of calculation and statistics are collected. 1594 1595 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1596 :param details: how detailed should the information be? You should specify one of strings: 1597 `full` - shows full available information about portfolio status (by default), 1598 `positions` - shows only open positions, 1599 `digest` - show a short digest of the portfolio status, 1600 `analytics` - shows only the analytics section and the distribution of the portfolio by various categories, 1601 `orders` - shows only sections of open limits and stop orders. 1602 :return: dictionary with client's raw portfolio and some statistics. 1603 """ 1604 if self.accountId is None or not self.accountId: 1605 uLogger.error("Variable `accountId` must be defined for using this method!") 1606 raise Exception("Account ID required") 1607 1608 view = { 1609 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1610 "headers": {}, # list of dictionaries, response headers without "positions" section 1611 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1612 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1613 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1614 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1615 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1616 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1617 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1618 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1619 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1620 }, 1621 "stat": { # --- some statistics calculated using "raw" sections: 1622 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1623 "availableRUB": 0., # available rubles (without other currencies) 1624 "blockedRUB": 0., # blocked sum in Russian Rouble 1625 "totalChangesRUB": 0., # changes for all open trades in RUB 1626 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1627 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1628 "sharesCostRUB": 0., # costs of all shares in RUB 1629 "bondsCostRUB": 0., # costs of all bonds in RUB 1630 "etfsCostRUB": 0., # costs of all etfs in RUB 1631 "futuresCostRUB": 0., # costs of all futures in RUB 1632 "Currencies": [], # list of dictionaries of all currencies statistics 1633 "Shares": [], # list of dictionaries of all shares statistics 1634 "Bonds": [], # list of dictionaries of all bonds statistics 1635 "Etfs": [], # list of dictionaries of all etfs statistics 1636 "Futures": [], # list of dictionaries of all futures statistics 1637 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1638 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1639 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1640 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1641 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1642 }, 1643 "analytics": { # --- some analytics of portfolio: 1644 "distrByAssets": {}, # portfolio distribution by assets 1645 "distrByCompanies": {}, # portfolio distribution by companies 1646 "distrBySectors": {}, # portfolio distribution by sectors 1647 "distrByCurrencies": {}, # portfolio distribution by currencies 1648 "distrByCountries": {}, # portfolio distribution by countries 1649 } 1650 } 1651 1652 details = details.lower() 1653 availableDetails = ["full", "positions", "digest", "analytics", "orders"] 1654 if details not in availableDetails: 1655 details = "full" 1656 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1657 1658 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1659 1660 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1661 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1662 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending orders (list) 1663 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1664 1665 # save response headers without "positions" section: 1666 for key in portfolioResponse.keys(): 1667 if key != "positions": 1668 view["raw"]["headers"][key] = portfolioResponse[key] 1669 1670 else: 1671 continue 1672 1673 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1674 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1675 for item in portfolioResponse["positions"]: 1676 if item["instrumentType"] == "currency": 1677 self.figi = item["figi"] 1678 curr = self.SearchByFIGI(requestPrice=False) 1679 1680 # current price of currency in RUB: 1681 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1682 "name": curr["name"], 1683 "currentPrice": NanoToFloat( 1684 item["currentPrice"]["units"], 1685 item["currentPrice"]["nano"] 1686 ), 1687 } 1688 1689 view["raw"]["Currencies"].append(item) 1690 1691 elif item["instrumentType"] == "share": 1692 view["raw"]["Shares"].append(item) 1693 1694 elif item["instrumentType"] == "bond": 1695 view["raw"]["Bonds"].append(item) 1696 1697 elif item["instrumentType"] == "etf": 1698 view["raw"]["Etfs"].append(item) 1699 1700 elif item["instrumentType"] == "futures": 1701 view["raw"]["Futures"].append(item) 1702 1703 else: 1704 continue 1705 1706 # how many volume of currencies (by ISO currency name) are blocked: 1707 for item in view["raw"]["positions"]["blocked"]: 1708 blocked = NanoToFloat(item["units"], item["nano"]) 1709 if blocked > 0: 1710 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1711 1712 # how many volume of instruments (by FIGI) are blocked: 1713 for item in view["raw"]["positions"]["securities"]: 1714 blocked = int(item["blocked"]) 1715 if blocked > 0: 1716 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1717 1718 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1719 1720 if "rub" in allBlocked.keys(): 1721 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1722 1723 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1724 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1725 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1726 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1727 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1728 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1729 view["stat"]["portfolioCostRUB"] = sum([ 1730 view["stat"]["allCurrenciesCostRUB"], 1731 view["stat"]["sharesCostRUB"], 1732 view["stat"]["bondsCostRUB"], 1733 view["stat"]["etfsCostRUB"], 1734 view["stat"]["futuresCostRUB"], 1735 ]) 1736 1737 # --- calculating some portfolio statistics: 1738 byComp = {} # distribution by companies 1739 bySect = {} # distribution by sectors 1740 byCurr = {} # distribution by currencies (include RUB) 1741 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1742 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1743 1744 for item in portfolioResponse["positions"]: 1745 self.figi = item["figi"] 1746 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1747 1748 if instrument: 1749 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1750 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1751 1752 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1753 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1754 1755 else: 1756 blocked = 0 1757 1758 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1759 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1760 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1761 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1762 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1763 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1764 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1765 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1766 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1767 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1768 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1769 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1770 1771 statData = { 1772 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1773 "ticker": instrument["ticker"], # ticker by FIGI 1774 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1775 "volume": volume, # available volume of instrument 1776 "lots": lots, # volume in lots of instrument 1777 "direction": direction, # direction of an instrument's position: short or long 1778 "blocked": blocked, # blocked volume of currency or instrument 1779 "currentPrice": curPrice, # current instrument's price in basic asset 1780 "average": average, # current average position price 1781 "cost": cost, # current cost of all volume of instrument in basic asset 1782 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1783 "costRUB": costRUB, # cost of instrument in ruble 1784 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1785 "profit": profit, # expected profit at current moment 1786 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1787 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1788 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1789 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1790 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1791 "step": instrument["step"], # minimum price increment 1792 } 1793 1794 # adding distribution by unique countries: 1795 if statData["country"] not in byCountry.keys(): 1796 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1797 1798 else: 1799 byCountry[statData["country"]]["cost"] += costRUB 1800 byCountry[statData["country"]]["percent"] += percentCostRUB 1801 1802 if item["instrumentType"] != "currency": 1803 # adding distribution by unique companies: 1804 if statData["name"]: 1805 if statData["name"] not in byComp.keys(): 1806 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1807 1808 else: 1809 byComp[statData["name"]]["cost"] += costRUB 1810 byComp[statData["name"]]["percent"] += percentCostRUB 1811 1812 # adding distribution by unique sectors: 1813 if statData["sector"] not in bySect.keys(): 1814 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1815 1816 else: 1817 bySect[statData["sector"]]["cost"] += costRUB 1818 bySect[statData["sector"]]["percent"] += percentCostRUB 1819 1820 # adding distribution by unique currencies: 1821 if currency not in byCurr.keys(): 1822 byCurr[currency] = { 1823 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1824 "cost": costRUB, 1825 "percent": percentCostRUB 1826 } 1827 1828 else: 1829 byCurr[currency]["cost"] += costRUB 1830 byCurr[currency]["percent"] += percentCostRUB 1831 1832 # saving statistics for every instrument: 1833 if item["instrumentType"] == "currency": 1834 view["stat"]["Currencies"].append(statData) 1835 1836 # update dict with free funds for trading (total - blocked) by currencies 1837 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1838 view["stat"]["funds"][currency] = { 1839 "total": volume, 1840 "totalCostRUB": costRUB, # total volume cost in rubles 1841 "free": volume - blocked, 1842 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1843 } 1844 1845 elif item["instrumentType"] == "share": 1846 view["stat"]["Shares"].append(statData) 1847 1848 elif item["instrumentType"] == "bond": 1849 view["stat"]["Bonds"].append(statData) 1850 1851 elif item["instrumentType"] == "etf": 1852 view["stat"]["Etfs"].append(statData) 1853 1854 elif item["instrumentType"] == "Futures": 1855 view["stat"]["Futures"].append(statData) 1856 1857 else: 1858 continue 1859 1860 # total changes in Russian Ruble: 1861 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1862 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1863 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1864 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1865 view["stat"]["funds"]["rub"] = { 1866 "total": view["stat"]["availableRUB"], 1867 "totalCostRUB": view["stat"]["availableRUB"], 1868 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1869 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1870 } 1871 1872 # --- pending orders sector data: 1873 uniquePendingOrders = [] 1874 uniquePendingOrdersFIGIs = [] 1875 for item in view["raw"]["orders"]: 1876 if item["figi"] not in uniquePendingOrdersFIGIs: 1877 uniquePendingOrdersFIGIs.append(item["figi"]) 1878 uniquePendingOrders.append(item) 1879 1880 for item in uniquePendingOrders: 1881 self.figi = item["figi"] 1882 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1883 1884 if instrument: 1885 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1886 orderType = TKS_ORDER_TYPES[item["orderType"]] 1887 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1888 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1889 1890 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1891 if item["direction"] == "ORDER_DIRECTION_BUY": 1892 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1893 1894 else: 1895 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1896 1897 # requested price for order execution: 1898 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1899 1900 # necessary changes in percent to reach target from current price: 1901 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1902 1903 view["stat"]["orders"].append({ 1904 "orderID": item["orderId"], # orderId number parameter of current order 1905 "figi": item["figi"], # FIGI identification 1906 "ticker": instrument["ticker"], # ticker name by FIGI 1907 "lotsRequested": item["lotsRequested"], # requested lots value 1908 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1909 "currentPrice": lastPrice, # current instrument's price for defined action 1910 "targetPrice": target, # requested price for order execution in base currency 1911 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1912 "percentChanges": changes, # changes in percent to target from current price 1913 "currency": item["currency"], # instrument's currency name 1914 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1915 "type": orderType, # type of order from TKS_ORDER_TYPES 1916 "status": orderState, # order status from TKS_ORDER_STATES 1917 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1918 }) 1919 1920 # --- stop orders sector data: 1921 uniqueStopOrders = [] 1922 uniqueStopOrdersFIGIs = [] 1923 for item in view["raw"]["stopOrders"]: 1924 if item["figi"] not in uniqueStopOrdersFIGIs: 1925 uniqueStopOrdersFIGIs.append(item["figi"]) 1926 uniqueStopOrders.append(item) 1927 1928 for item in uniqueStopOrders: 1929 self.figi = item["figi"] 1930 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1931 1932 if instrument: 1933 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1934 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1935 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1936 1937 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1938 if "expirationTime" in item.keys(): 1939 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1940 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1941 1942 else: 1943 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1944 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1945 1946 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1947 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1948 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1949 1950 else: 1951 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1952 1953 # requested price when stop-order executed: 1954 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1955 1956 # price for limit-order, set up when stop-order executed: 1957 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1958 1959 # necessary changes in percent to reach target from current price: 1960 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1961 1962 view["stat"]["stopOrders"].append({ 1963 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1964 "figi": item["figi"], # FIGI identification 1965 "ticker": instrument["ticker"], # ticker name by FIGI 1966 "lotsRequested": item["lotsRequested"], # requested lots value 1967 "currentPrice": lastPrice, # current instrument's price for defined action 1968 "targetPrice": target, # requested price for stop-order execution in base currency 1969 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1970 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1971 "percentChanges": changes, # changes in percent to target from current price 1972 "currency": item["currency"], # instrument's currency name 1973 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1974 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 1975 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 1976 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 1977 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 1978 }) 1979 1980 # --- calculating data for analytics section: 1981 # portfolio distribution by assets: 1982 view["analytics"]["distrByAssets"] = { 1983 "Ruble": { 1984 "uniques": 1, 1985 "cost": view["stat"]["availableRUB"], 1986 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1987 }, 1988 "Currencies": { 1989 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 1990 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 1991 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1992 }, 1993 "Shares": { 1994 "uniques": len(view["stat"]["Shares"]), 1995 "cost": view["stat"]["sharesCostRUB"], 1996 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1997 }, 1998 "Bonds": { 1999 "uniques": len(view["stat"]["Bonds"]), 2000 "cost": view["stat"]["bondsCostRUB"], 2001 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2002 }, 2003 "Etfs": { 2004 "uniques": len(view["stat"]["Etfs"]), 2005 "cost": view["stat"]["etfsCostRUB"], 2006 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2007 }, 2008 "Futures": { 2009 "uniques": len(view["stat"]["Futures"]), 2010 "cost": view["stat"]["futuresCostRUB"], 2011 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2012 }, 2013 } 2014 2015 # portfolio distribution by companies: 2016 view["analytics"]["distrByCompanies"]["All money cash"] = { 2017 "ticker": "", 2018 "cost": view["stat"]["allCurrenciesCostRUB"], 2019 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2020 } 2021 view["analytics"]["distrByCompanies"].update(byComp) 2022 2023 # portfolio distribution by sectors: 2024 view["analytics"]["distrBySectors"]["All money cash"] = { 2025 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2026 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2027 } 2028 view["analytics"]["distrBySectors"].update(bySect) 2029 2030 # portfolio distribution by currencies: 2031 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2032 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2033 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2034 2035 view["analytics"]["distrByCurrencies"].update(byCurr) 2036 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2037 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2038 2039 # portfolio distribution by countries: 2040 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2041 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2042 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2043 2044 view["analytics"]["distrByCountries"].update(byCountry) 2045 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2046 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2047 2048 # --- Prepare text statistics overview in human-readable: 2049 if show: 2050 # Whatever the value `details`, header not changes: 2051 info = [ 2052 "# Client's portfolio\n\n", 2053 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 2054 "* **Account ID:** [{}]\n".format(self.accountId), 2055 ] 2056 2057 if details in ["full", "positions", "digest"]: 2058 info.extend([ 2059 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2060 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2061 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2062 view["stat"]["totalChangesRUB"], 2063 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2064 view["stat"]["totalChangesPercentRUB"], 2065 ), 2066 ]) 2067 2068 if details in ["full", "positions"]: 2069 info.extend([ 2070 "## Open positions\n\n", 2071 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2072 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2073 "| Ruble | {:>31} | | | | | |\n".format( 2074 "{:.2f} ({:.2f}) rub".format( 2075 view["stat"]["availableRUB"], 2076 view["stat"]["blockedRUB"], 2077 ) 2078 ) 2079 ]) 2080 2081 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2082 return [ 2083 "| | | | | | | |\n", 2084 "| {:<27} | | | | | {:>19} | |\n".format( 2085 noTradeStr if noTradeStr else typeStr, 2086 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2087 ), 2088 ] 2089 2090 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2091 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2092 "{} [{}]".format(data["ticker"], data["figi"]), 2093 "{:.2f} ({:.2f}) {}".format( 2094 data["volume"], 2095 data["blocked"], 2096 data["currency"], 2097 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2098 data["volume"], 2099 data["blocked"], 2100 ), 2101 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2102 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2103 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2104 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2105 "{}{:.2f} {} ({}{:.2f}%)".format( 2106 "+" if data["profit"] > 0 else "", 2107 data["profit"], data["baseCurrencyName"], 2108 "+" if data["percentProfit"] > 0 else "", 2109 data["percentProfit"], 2110 ), 2111 ) 2112 2113 # --- Show currencies section: 2114 if view["stat"]["Currencies"]: 2115 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2116 for item in view["stat"]["Currencies"]: 2117 info.append(_InfoStr(item, showCurrencyName=True)) 2118 2119 else: 2120 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2121 2122 # --- Show shares section: 2123 if view["stat"]["Shares"]: 2124 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2125 2126 for item in view["stat"]["Shares"]: 2127 info.append(_InfoStr(item)) 2128 2129 else: 2130 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2131 2132 # --- Show bonds section: 2133 if view["stat"]["Bonds"]: 2134 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2135 2136 for item in view["stat"]["Bonds"]: 2137 info.append(_InfoStr(item)) 2138 2139 else: 2140 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2141 2142 # --- Show etfs section: 2143 if view["stat"]["Etfs"]: 2144 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2145 2146 for item in view["stat"]["Etfs"]: 2147 info.append(_InfoStr(item)) 2148 2149 else: 2150 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2151 2152 # --- Show futures section: 2153 if view["stat"]["Futures"]: 2154 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2155 2156 for item in view["stat"]["Futures"]: 2157 info.append(_InfoStr(item)) 2158 2159 else: 2160 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2161 2162 if details in ["full", "orders"]: 2163 # --- Show pending orders section: 2164 if view["stat"]["orders"]: 2165 info.extend([ 2166 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2167 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2168 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2169 ]) 2170 2171 for item in view["stat"]["orders"]: 2172 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2173 "{} [{}]".format(item["ticker"], item["figi"]), 2174 item["orderID"], 2175 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2176 "{} {} ({}{:.2f}%)".format( 2177 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2178 item["baseCurrencyName"], 2179 "+" if item["percentChanges"] > 0 else "", 2180 float(item["percentChanges"]), 2181 ), 2182 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2183 item["action"], 2184 item["type"], 2185 item["date"], 2186 )) 2187 2188 else: 2189 info.append("\n## Total pending limit-orders: 0\n") 2190 2191 # --- Show stop orders section: 2192 if view["stat"]["stopOrders"]: 2193 info.extend([ 2194 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2195 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2196 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2197 ]) 2198 2199 for item in view["stat"]["stopOrders"]: 2200 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2201 "{} [{}]".format(item["ticker"], item["figi"]), 2202 item["orderID"], 2203 item["lotsRequested"], 2204 "{} {} ({}{:.2f}%)".format( 2205 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2206 item["baseCurrencyName"], 2207 "+" if item["percentChanges"] > 0 else "", 2208 float(item["percentChanges"]), 2209 ), 2210 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2211 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2212 item["action"], 2213 item["type"], 2214 item["expType"], 2215 item["createDate"], 2216 item["expDate"], 2217 )) 2218 2219 else: 2220 info.append("\n## Total stop-orders: 0\n") 2221 2222 if details in ["full", "analytics"]: 2223 # -- Show analytics section: 2224 if view["stat"]["portfolioCostRUB"] > 0: 2225 info.extend([ 2226 "\n# Analytics\n" 2227 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2228 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2229 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2230 view["stat"]["totalChangesRUB"], 2231 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2232 view["stat"]["totalChangesPercentRUB"], 2233 ), 2234 "\n## Portfolio distribution by assets\n" 2235 "\n| Type | Uniques | Percent | Current cost |\n", 2236 "|------------|---------|---------|--------------------|\n", 2237 ]) 2238 2239 for key in view["analytics"]["distrByAssets"].keys(): 2240 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2241 info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format( 2242 key, 2243 view["analytics"]["distrByAssets"][key]["uniques"], 2244 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2245 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2246 )) 2247 2248 maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()]) 2249 info.extend([ 2250 "\n## Portfolio distribution by companies\n" 2251 "\n| Company{} | Percent | Current cost |\n".format(" " * (maxLenNames - 7)), 2252 "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)), 2253 ]) 2254 2255 for company in view["analytics"]["distrByCompanies"].keys(): 2256 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2257 nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) 2258 info.append("| {} | {:<7} | {:<18} |\n".format( 2259 "{}{}{}".format( 2260 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2261 company, 2262 "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)), 2263 ), 2264 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2265 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2266 )) 2267 2268 maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()]) 2269 info.extend([ 2270 "\n## Portfolio distribution by sectors\n" 2271 "\n| Sector{} | Percent | Current cost |\n".format(" " * (maxLenSectors - 6)), 2272 "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)), 2273 ]) 2274 2275 for sector in view["analytics"]["distrBySectors"].keys(): 2276 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2277 info.append("| {}{} | {:<7} | {:<18} |\n".format( 2278 sector, 2279 "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)), 2280 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2281 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2282 )) 2283 2284 maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()]) 2285 info.extend([ 2286 "\n## Portfolio distribution by currencies\n" 2287 "\n| Instruments currencies{} | Percent | Current cost |\n".format(" " * (maxLenMoney - 22)), 2288 "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)), 2289 ]) 2290 2291 for curr in view["analytics"]["distrByCurrencies"].keys(): 2292 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2293 nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"]) 2294 info.append("| {} | {:<7} | {:<18} |\n".format( 2295 "[{}] {}{}".format( 2296 curr, 2297 view["analytics"]["distrByCurrencies"][curr]["name"], 2298 "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen), 2299 ), 2300 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2301 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2302 )) 2303 2304 maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()])) 2305 info.extend([ 2306 "\n## Portfolio distribution by countries\n" 2307 "\n| Assets by country{} | Percent | Current cost |\n".format(" " * (maxLenCountry - 17)), 2308 "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)), 2309 ]) 2310 2311 for country in view["analytics"]["distrByCountries"].keys(): 2312 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2313 nameLen = len(country) 2314 info.append("| {} | {:<7} | {:<18} |\n".format( 2315 "{}{}".format( 2316 country, 2317 "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen), 2318 ), 2319 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2320 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2321 )) 2322 2323 infoText = "".join(info) 2324 2325 uLogger.info(infoText) 2326 2327 if details == "full" and self.overviewFile: 2328 filename = self.overviewFile 2329 2330 elif details == "digest" and self.overviewDigestFile: 2331 filename = self.overviewDigestFile 2332 2333 elif details == "positions" and self.overviewPositionsFile: 2334 filename = self.overviewPositionsFile 2335 2336 elif details == "orders" and self.overviewOrdersFile: 2337 filename = self.overviewOrdersFile 2338 2339 elif details == "analytics" and self.overviewAnalyticsFile: 2340 filename = self.overviewAnalyticsFile 2341 2342 else: 2343 filename = "" 2344 2345 if filename: 2346 with open(filename, "w", encoding="UTF-8") as fH: 2347 fH.write(infoText) 2348 2349 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2350 2351 return view
Get portfolio: all open positions, orders and some statistics for current accountId.
If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile
are defined then also save information to file.
WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen show more debug information. - details: how detailed should the information be? You should specify one of strings:
full- shows full available information about portfolio status (by default),positions- shows only open positions,digest- show a short digest of the portfolio status,analytics- shows only the analytics section and the distribution of the portfolio by various categories,orders- shows only sections of open limits and stop orders.
Returns
dictionary with client's raw portfolio and some statistics.
2353 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple: 2354 """ 2355 Returns history operations between two given dates for current `accountId`. 2356 If `reportFile` string is not empty then also save human-readable report. 2357 Shows some statistical data of closed positions. 2358 2359 :param start: see docstring in `GetDatesAsString()` method 2360 :param end: see docstring in `GetDatesAsString()` method 2361 :param show: if `True` then also prints all records to the console. 2362 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2363 :return: original list of dictionaries with history of deals records from API ("operations" key): 2364 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2365 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2366 """ 2367 if self.accountId is None or not self.accountId: 2368 uLogger.error("Variable `accountId` must be defined for using this method!") 2369 raise Exception("Account ID required") 2370 2371 startDate, endDate = GetDatesAsString(start, end) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2372 2373 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2374 2375 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2376 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2377 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2378 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2379 customStat = {} # custom statistics in additional to responseJSON 2380 2381 # --- output report in human-readable format: 2382 if show or self.reportFile: 2383 splitLine1 = "| | | | | |\n" # Summary section 2384 splitLine2 = "| | | | | | | | |\n" # Operations section 2385 nextDay = "" 2386 2387 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2388 2389 if len(ops) > 0: 2390 customStat = { 2391 "opsCount": 0, # total operations count 2392 "buyCount": 0, # buy operations 2393 "sellCount": 0, # sell operations 2394 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2395 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2396 "payIn": {"rub": 0.}, # Deposit brokerage account 2397 "payOut": {"rub": 0.}, # Withdrawals 2398 "divs": {"rub": 0.}, # Dividends income 2399 "coupons": {"rub": 0.}, # Coupon's income 2400 "brokerCom": {"rub": 0.}, # Service commissions 2401 "serviceCom": {"rub": 0.}, # Service commissions 2402 "marginCom": {"rub": 0.}, # Margin commissions 2403 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2404 } 2405 2406 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2407 for item in ops: 2408 if item["state"] == "OPERATION_STATE_EXECUTED": 2409 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2410 2411 # count buy operations: 2412 if "_BUY" in item["operationType"]: 2413 customStat["buyCount"] += 1 2414 2415 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2416 customStat["buyTotal"][item["payment"]["currency"]] += payment 2417 2418 else: 2419 customStat["buyTotal"][item["payment"]["currency"]] = payment 2420 2421 # count sell operations: 2422 elif "_SELL" in item["operationType"]: 2423 customStat["sellCount"] += 1 2424 2425 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2426 customStat["sellTotal"][item["payment"]["currency"]] += payment 2427 2428 else: 2429 customStat["sellTotal"][item["payment"]["currency"]] = payment 2430 2431 # count incoming operations: 2432 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2433 if item["payment"]["currency"] in customStat["payIn"].keys(): 2434 customStat["payIn"][item["payment"]["currency"]] += payment 2435 2436 else: 2437 customStat["payIn"][item["payment"]["currency"]] = payment 2438 2439 # count withdrawals operations: 2440 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2441 if item["payment"]["currency"] in customStat["payOut"].keys(): 2442 customStat["payOut"][item["payment"]["currency"]] += payment 2443 2444 else: 2445 customStat["payOut"][item["payment"]["currency"]] = payment 2446 2447 # count dividends income: 2448 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2449 if item["payment"]["currency"] in customStat["divs"].keys(): 2450 customStat["divs"][item["payment"]["currency"]] += payment 2451 2452 else: 2453 customStat["divs"][item["payment"]["currency"]] = payment 2454 2455 # count coupon's income: 2456 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2457 if item["payment"]["currency"] in customStat["coupons"].keys(): 2458 customStat["coupons"][item["payment"]["currency"]] += payment 2459 2460 else: 2461 customStat["coupons"][item["payment"]["currency"]] = payment 2462 2463 # count broker commissions: 2464 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2465 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2466 customStat["brokerCom"][item["payment"]["currency"]] += payment 2467 2468 else: 2469 customStat["brokerCom"][item["payment"]["currency"]] = payment 2470 2471 # count service commissions: 2472 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2473 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2474 customStat["serviceCom"][item["payment"]["currency"]] += payment 2475 2476 else: 2477 customStat["serviceCom"][item["payment"]["currency"]] = payment 2478 2479 # count margin commissions: 2480 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2481 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2482 customStat["marginCom"][item["payment"]["currency"]] += payment 2483 2484 else: 2485 customStat["marginCom"][item["payment"]["currency"]] = payment 2486 2487 # count withholding taxes: 2488 elif "_TAX" in item["operationType"]: 2489 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2490 customStat["allTaxes"][item["payment"]["currency"]] += payment 2491 2492 else: 2493 customStat["allTaxes"][item["payment"]["currency"]] = payment 2494 2495 else: 2496 continue 2497 2498 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2499 2500 # --- view "Actions" lines: 2501 info.extend([ 2502 "| 1 | 2 | 3 | 4 | 5 |\n", 2503 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2504 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2505 "| | Buy: {:<22} | {:<28} | | |\n".format( 2506 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2507 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2508 ), 2509 "| | Sell: {:<21} | {:<28} | | |\n".format( 2510 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2511 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2512 ), 2513 ]) 2514 2515 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2516 for key in opsKeys: 2517 if key == "rub": 2518 continue 2519 2520 info.extend([ 2521 "| | | {:<28} | | |\n".format( 2522 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2523 ), 2524 "| | | {:<28} | | |\n".format( 2525 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2526 ), 2527 ]) 2528 2529 info.append(splitLine1) 2530 2531 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2532 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2533 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2534 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2535 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2536 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2537 ) 2538 2539 # --- view "Payments" lines: 2540 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2541 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2542 2543 for key in paymentsKeys: 2544 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2545 2546 info.append(splitLine1) 2547 2548 # --- view "Commissions and taxes" lines: 2549 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2550 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2551 2552 for key in comKeys: 2553 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2554 2555 info.append(splitLine1) 2556 2557 info.extend([ 2558 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2559 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2560 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2561 ]) 2562 2563 else: 2564 info.append("Broker returned no operations during this period\n") 2565 2566 # --- view "Operations" section: 2567 for item in ops: 2568 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2569 continue 2570 2571 else: 2572 self.figi = item["figi"] if item["figi"] else "" 2573 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2574 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2575 2576 # group of deals during one day: 2577 if nextDay and item["date"].split("T")[0] != nextDay: 2578 info.append(splitLine2) 2579 nextDay = "" 2580 2581 else: 2582 nextDay = item["date"].split("T")[0] # saving current day for splitting 2583 2584 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2585 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2586 self.figi if self.figi else "—", 2587 instrument["ticker"] if instrument else "—", 2588 instrument["type"] if instrument else "—", 2589 item["quantity"] if int(item["quantity"]) > 0 else "—", 2590 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2591 TKS_OPERATION_STATES[item["state"]], 2592 TKS_OPERATION_TYPES[item["operationType"]], 2593 )) 2594 2595 infoText = "".join(info) 2596 2597 if show: 2598 uLogger.info(infoText) 2599 2600 if self.reportFile: 2601 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2602 fH.write(infoText) 2603 2604 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2605 2606 return ops, customStat
Returns history operations between two given dates for current accountId.
If reportFile string is not empty then also save human-readable report.
Shows some statistical data of closed positions.
Parameters
- start: see docstring in
GetDatesAsString()method - end: see docstring in
GetDatesAsString()method - show: if
Truethen also prints all records to the console. - showCancelled: if
Falsethen remove information about cancelled operations from the deals report.
Returns
original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2608 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2609 """ 2610 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2611 2612 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2613 Warning! Broker server used ISO UTC time by default. 2614 2615 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2616 Also, `historyFile` used to update history with `onlyMissing` parameter. 2617 2618 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2619 2620 :param start: see docstring in `GetDatesAsString()` method. 2621 :param end: see docstring in `GetDatesAsString()` method. 2622 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2623 `"hour"`, `"day"`. Default: `"hour"`. 2624 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2625 False by default. Warning! History appends only from last candle to current time 2626 with always update last candle! 2627 :param csvSep: separator if csv-file is used, `,` by default. 2628 :param show: if `True` then also prints Pandas DataFrame to the console. 2629 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2630 `["date", "time", "open", "high", "low", "close", "volume"]`. 2631 """ 2632 strStartDate, strEndDate = GetDatesAsString(start, end) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2633 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2634 history = None # empty pandas object for history 2635 2636 if interval not in TKS_CANDLE_INTERVALS.keys(): 2637 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2638 raise Exception("Incorrect value") 2639 2640 if not (self.ticker or self.figi): 2641 uLogger.error("Ticker or FIGI must be defined!") 2642 raise Exception("Ticker or FIGI required") 2643 2644 if self.ticker and not self.figi: 2645 instrumentByTicker = self.SearchByTicker(requestPrice=False, debug=False) 2646 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2647 2648 if self.figi and not self.ticker: 2649 instrumentByFIGI = self.SearchByFIGI(requestPrice=False, debug=False) 2650 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2651 2652 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2653 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2654 if interval.lower() != "day": 2655 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59 2656 2657 delta = dtEnd - dtStart # current UTC time minus last time in file 2658 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2659 2660 # calculate history length in candles: 2661 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2662 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2663 length += 1 # to avoid fraction time 2664 2665 # calculate data blocks count: 2666 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2667 2668 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2669 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2670 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2671 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2672 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2673 2674 tempOld = None # pandas object for old history, if --only-missing key present 2675 lastTime = None # datetime object of last old candle in file 2676 2677 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2678 uLogger.debug("--only-missing key present, add only last missing candles...") 2679 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2680 2681 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2682 2683 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2684 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2685 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2686 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2687 2688 # get last datetime object from last string in file or minus 1 delta if file is empty: 2689 if len(tempOld) > 0: 2690 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2691 2692 else: 2693 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2694 2695 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2696 2697 responseJSONs = [] # raw history blocks of data 2698 2699 blockEnd = dtEnd 2700 for item in range(blocks): 2701 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2702 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2703 2704 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2705 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2706 )) 2707 2708 if blockStart == blockEnd: 2709 uLogger.debug("Skipped this zero-length block...") 2710 2711 else: 2712 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2713 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2714 self.body = str({ 2715 "figi": self.figi, 2716 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2717 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2718 "interval": TKS_CANDLE_INTERVALS[interval][0] 2719 }) 2720 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1, debug=False) 2721 2722 if "code" in responseJSON.keys(): 2723 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2724 2725 else: 2726 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2727 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2728 2729 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2730 2731 blockEnd = blockStart 2732 2733 printCount = len(responseJSONs) # candles to show in console 2734 if responseJSONs: 2735 tempHistory = pd.DataFrame( 2736 data={ 2737 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2738 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2739 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2740 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2741 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2742 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2743 "volume": [int(item["volume"]) for item in responseJSONs], 2744 }, 2745 index=range(len(responseJSONs)), 2746 columns=["date", "time", "open", "high", "low", "close", "volume"], 2747 ) 2748 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2749 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2750 2751 # append only newest candles to old history if --only-missing key present: 2752 if onlyMissing and tempOld is not None and lastTime is not None: 2753 index = 0 # find start index in tempHistory data: 2754 2755 for i, item in tempHistory.iterrows(): 2756 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2757 2758 if curTime == lastTime: 2759 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2760 index = i 2761 printCount = index + 1 2762 break 2763 2764 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2765 2766 else: 2767 history = tempHistory # if no `--only-missing` key then load full data from server 2768 2769 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2770 2771 if history is not None and not history.empty: 2772 if show: 2773 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2774 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2775 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2776 )) 2777 2778 else: 2779 uLogger.warning("Received an empty candles history!") 2780 2781 if self.historyFile is not None: 2782 if history is not None and not history.empty: 2783 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2784 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2785 2786 else: 2787 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2788 2789 else: 2790 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2791 2792 return history
This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).
History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01.
Warning! Broker server used ISO UTC time by default.
If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame.
Also, historyFile used to update history with onlyMissing parameter.
See also: LoadHistory() and ShowHistoryChart() methods.
Parameters
- start: see docstring in
GetDatesAsString()method. - end: see docstring in
GetDatesAsString()method. - interval: this is a candle interval. Current available values are
"1min","5min","15min","hour","day". Default:"hour". - onlyMissing: if
Truethen add only last missing candles, do not request all history length fromstart. False by default. Warning! History appends only from last candle to current time with always update last candle! - csvSep: separator if csv-file is used,
,by default. - show: if
Truethen also prints Pandas DataFrame to the console.
Returns
Pandas DataFrame with prices history. Headers of columns are defined by default:
["date", "time", "open", "high", "low", "close", "volume"].
2794 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2795 """ 2796 Load candles history from csv-file and return Pandas DataFrame object. 2797 2798 See also: `History()` and `ShowHistoryChart()` methods. 2799 2800 :param filePath: path to csv-file to open. 2801 """ 2802 loadedHistory = None # init candles data object 2803 2804 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2805 2806 if os.path.exists(filePath): 2807 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2808 2809 tfStr = self.priceModel.FormattedDelta( 2810 self.priceModel.timeframe, 2811 "{days} days {hours}h {minutes}m {seconds}s", 2812 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2813 self.priceModel.timeframe, 2814 "{hours}h {minutes}m {seconds}s", 2815 ) 2816 2817 if loadedHistory is not None and not loadedHistory.empty: 2818 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2819 len(loadedHistory), 2820 tfStr, 2821 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2822 ) 2823 2824 else: 2825 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2826 2827 else: 2828 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2829 2830 return loadedHistory
Load candles history from csv-file and return Pandas DataFrame object.
See also: History() and ShowHistoryChart() methods.
Parameters
- filePath: path to csv-file to open.
2832 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2833 """ 2834 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2835 2836 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2837 Default: `index.html` (both for interact and non-interact candlesticks chart). 2838 2839 See also: `History()` and `LoadHistory()` methods. 2840 2841 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2842 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2843 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2844 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2845 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2846 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2847 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2848 """ 2849 if isinstance(candles, str): 2850 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2851 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2852 2853 elif isinstance(candles, pd.DataFrame): 2854 self.priceModel.prices = candles # set candles chain from variable 2855 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2856 2857 if "datetime" not in candles.columns: 2858 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2859 2860 else: 2861 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2862 raise Exception("Incorrect value") 2863 2864 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2865 2866 if interact: 2867 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2868 2869 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2870 2871 else: 2872 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2873 2874 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2875 2876 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart.
Default: index.html (both for interact and non-interact candlesticks chart).
See also: History() and LoadHistory() methods.
Parameters
- candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
- interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters If False then chain of candlesticks will render as not interactive Google Candlestick chart. See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
- openInBrowser: if True then immediately open chart in default browser, otherwise only path to
html-file prints to console. False by default, to avoid issues with
permissions deniedto html-file.
2878 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2879 """ 2880 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2881 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2882 2883 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2884 2885 :param operation: string "Buy" or "Sell". 2886 :param lots: volume, integer count of lots >= 1. 2887 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2888 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2889 :param expDate: string "Undefined" by default or local date in future, 2890 it is a string with format `%Y-%m-%d %H:%M:%S`. 2891 :return: JSON with response from broker server. 2892 """ 2893 if self.accountId is None or not self.accountId: 2894 uLogger.error("Variable `accountId` must be defined for using this method!") 2895 raise Exception("Account ID required") 2896 2897 if operation is None or not operation or operation not in ("Buy", "Sell"): 2898 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2899 raise Exception("Incorrect value") 2900 2901 if lots is None or lots < 1: 2902 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2903 lots = 1 2904 2905 if tp is None or tp < 0: 2906 tp = 0 2907 2908 if sl is None or sl < 0: 2909 sl = 0 2910 2911 if expDate is None or not expDate: 2912 expDate = "Undefined" 2913 2914 if not (self.ticker or self.figi): 2915 uLogger.error("Ticker or FIGI must be defined!") 2916 raise Exception("Ticker or FIGI required") 2917 2918 instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False) 2919 self.ticker = instrument["ticker"] 2920 self.figi = instrument["figi"] 2921 2922 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2923 2924 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2925 self.body = str({ 2926 "figi": self.figi, 2927 "quantity": str(lots), 2928 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2929 "accountId": str(self.accountId), 2930 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2931 }) 2932 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0, debug=False) 2933 2934 if "orderId" in response.keys(): 2935 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2936 operation, response["orderId"], 2937 self.ticker, self.figi, lots, 2938 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2939 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2940 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2941 )) 2942 2943 else: 2944 uLogger.warning("Not `oK` status received! Market order not created. See full debug log or try again and open order later.") 2945 2946 if tp > 0: 2947 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2948 2949 if sl > 0: 2950 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2951 2952 return response
Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().
Parameters
- operation: string "Buy" or "Sell".
- lots: volume, integer count of lots >= 1.
- tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter
targetPriceinself.Order(). - sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter
targetPriceinself.Order(). - expDate: string "Undefined" by default or local date in future,
it is a string with format
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
2954 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2955 """ 2956 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2957 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 2958 2959 See also: `Order()` and `Trade()` docstrings. 2960 2961 :param lots: volume, integer count of lots >= 1. 2962 :param tp: float > 0, take profit price of stop-order. 2963 :param sl: float > 0, stop loss price of stop-order. 2964 :param expDate: it's a local date in future. 2965 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2966 :return: JSON with response from broker server. 2967 """ 2968 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
2970 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2971 """ 2972 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 2973 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2974 2975 See also: `Order()` and `Trade()` docstrings. 2976 2977 :param lots: volume, integer count of lots >= 1. 2978 :param tp: float > 0, take profit price of stop-order. 2979 :param sl: float > 0, stop loss price of stop-order. 2980 :param expDate: it's a local date in the future. 2981 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2982 :return: JSON with response from broker server. 2983 """ 2984 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in the future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
2986 def CloseTrades(self, tickers: list, portfolio: dict = None) -> None: 2987 """ 2988 Close position of given instruments. 2989 2990 :param tickers: tickers list of instruments that must be closed. 2991 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 2992 This avoids unnecessary downloading data from the server. 2993 """ 2994 if not tickers: 2995 uLogger.info("Tickers list is empty, nothing to close.") 2996 2997 else: 2998 if portfolio is None or not portfolio: 2999 portfolio = self.Overview(show=False) 3000 3001 allOpenedTickers = [item["ticker"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3002 uLogger.debug("All opened instruments by it's tickers names: {}".format(allOpenedTickers)) 3003 3004 for ticker in tickers: 3005 if ticker not in allOpenedTickers: 3006 uLogger.warning("Instrument with ticker [{}] not in open positions list!".format(ticker)) 3007 continue 3008 3009 # search open trade info about instrument by ticker: 3010 instrument = {} 3011 for iType in TKS_INSTRUMENTS: 3012 if instrument: 3013 break 3014 3015 for item in portfolio["stat"][iType]: 3016 if item["ticker"] == ticker: 3017 instrument = item 3018 break 3019 3020 if instrument: 3021 self.ticker = ticker 3022 self.figi = instrument["figi"] 3023 3024 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3025 self.ticker, 3026 self.figi, 3027 int(instrument["volume"]), 3028 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3029 )) 3030 3031 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3032 3033 if tradeLots > 0: 3034 if instrument["blocked"] > 0: 3035 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3036 instrument["blocked"], 3037 self.ticker, 3038 tradeLots, 3039 )) 3040 3041 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3042 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3043 3044 else: 3045 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))
Close position of given instruments.
Parameters
- tickers: tickers list of instruments that must be closed.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3047 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3048 """ 3049 Close all positions of given instruments with defined type. 3050 3051 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3052 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3053 This avoids unnecessary downloading data from the server. 3054 """ 3055 if iType not in TKS_INSTRUMENTS: 3056 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3057 3058 else: 3059 if portfolio is None or not portfolio: 3060 portfolio = self.Overview(show=False) 3061 3062 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3063 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3064 3065 if tickers and portfolio: 3066 self.CloseTrades(tickers, portfolio) 3067 3068 else: 3069 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
Close all positions of given instruments with defined type.
Parameters
- iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3071 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3072 """ 3073 Universal method to create market or limit orders with all available parameters for current `accountId`. 3074 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3075 3076 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3077 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3078 3079 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3080 then broker immediately open market order as you can do simple --buy or --sell operations! 3081 3082 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3083 When current price will go up or down to target price value then broker opens a limit order. 3084 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3085 3086 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3087 3088 :param operation: string "Buy" or "Sell". 3089 :param orderType: string "Limit" or "Stop". 3090 :param lots: volume, integer count of lots >= 1. 3091 :param targetPrice: target price > 0. This is open trade price for limit order. 3092 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3093 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3094 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3095 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3096 Stop loss order always executed by market price. 3097 :param expDate: string "Undefined" by default or local date in future. 3098 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3099 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3100 A limit order has no expiration date, it lasts until the end of the trading day. 3101 :return: JSON with response from broker server. 3102 """ 3103 if self.accountId is None or not self.accountId: 3104 uLogger.error("Variable `accountId` must be defined for using this method!") 3105 raise Exception("Account ID required") 3106 3107 if operation is None or not operation or operation not in ("Buy", "Sell"): 3108 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3109 raise Exception("Incorrect value") 3110 3111 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3112 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3113 raise Exception("Incorrect value") 3114 3115 if lots is None or lots < 1: 3116 uLogger.error("You must define trade volume > 0: integer count of lots!") 3117 raise Exception("Incorrect value") 3118 3119 if targetPrice is None or targetPrice <= 0: 3120 uLogger.error("Target price for limit-order must be greater than 0!") 3121 raise Exception("Incorrect value") 3122 3123 if limitPrice is None or limitPrice <= 0: 3124 limitPrice = targetPrice 3125 3126 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3127 stopType = "Limit" 3128 3129 if expDate is None or not expDate: 3130 expDate = "Undefined" 3131 3132 if not (self.ticker or self.figi): 3133 uLogger.error("Tocker or FIGI must be defined!") 3134 raise Exception("Ticker or FIGI required") 3135 3136 response = {} 3137 instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False) 3138 self.ticker = instrument["ticker"] 3139 self.figi = instrument["figi"] 3140 3141 if orderType == "Limit": 3142 uLogger.debug( 3143 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3144 self.ticker, self.figi, 3145 operation, lots, targetPrice, instrument["currency"], 3146 )) 3147 3148 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3149 self.body = str({ 3150 "figi": self.figi, 3151 "quantity": str(lots), 3152 "price": FloatToNano(targetPrice), 3153 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3154 "accountId": str(self.accountId), 3155 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3156 }) 3157 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False) 3158 3159 if "orderId" in response.keys(): 3160 uLogger.info( 3161 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3162 response["orderId"], 3163 self.ticker, self.figi, 3164 operation, lots, targetPrice, instrument["currency"], 3165 )) 3166 3167 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3168 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3169 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3170 targetPrice, instrument["currency"], 3171 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3172 )) 3173 3174 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3175 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3176 targetPrice, instrument["currency"], 3177 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3178 )) 3179 3180 else: 3181 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.") 3182 3183 if orderType == "Stop": 3184 uLogger.debug( 3185 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3186 self.ticker, self.figi, 3187 operation, lots, 3188 targetPrice, instrument["currency"], 3189 limitPrice, instrument["currency"], 3190 stopType, expDate, 3191 )) 3192 3193 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3194 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3195 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3196 3197 body = { 3198 "figi": self.figi, 3199 "quantity": str(lots), 3200 "price": FloatToNano(limitPrice), 3201 "stopPrice": FloatToNano(targetPrice), 3202 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3203 "accountId": str(self.accountId), 3204 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3205 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3206 } 3207 3208 if expDateUTC: 3209 body["expireDate"] = expDateUTC 3210 3211 self.body = str(body) 3212 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False) 3213 3214 if "stopOrderId" in response.keys(): 3215 uLogger.info( 3216 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3217 response["stopOrderId"], 3218 self.ticker, self.figi, 3219 operation, lots, 3220 targetPrice, instrument["currency"], 3221 limitPrice, instrument["currency"], 3222 TKS_STOP_ORDER_TYPES[stopOrderType], 3223 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3224 )) 3225 3226 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3227 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3228 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3229 targetPrice, instrument["currency"], 3230 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3231 )) 3232 3233 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3234 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3235 targetPrice, instrument["currency"], 3236 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3237 )) 3238 3239 else: 3240 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.") 3241 3242 return response
Universal method to create market or limit orders with all available parameters for current accountId.
See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().
If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!
If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
Only one attempt and no retry for opens order. If network issue occurred you can create new request.
Parameters
- operation: string "Buy" or "Sell".
- orderType: string "Limit" or "Stop".
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
- limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
- stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns
JSON with response from broker server.
3244 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3245 """ 3246 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3247 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3248 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3249 See also: `Order()` docstring. 3250 3251 :param lots: volume, integer count of lots >= 1. 3252 :param targetPrice: target price > 0. This is open trade price for limit order. 3253 :return: JSON with response from broker server. 3254 """ 3255 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Buy limit-order (below current price). You must specify only 2 parameters:
lots and target price to open buy limit-order. If you try to create buy limit-order above current price then
broker immediately open Buy market order, such as if you do simple --buy operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3257 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3258 """ 3259 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3260 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3261 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3262 target price value then broker opens a limit order. See also: `Order()` docstring. 3263 3264 :param lots: volume, integer count of lots >= 1. 3265 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3266 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3267 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3268 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3269 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3270 :param expDate: string "Undefined" by default or local date in future. 3271 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3272 This date is converting to UTC format for server. 3273 :return: JSON with response from broker server. 3274 """ 3275 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order.
In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for buy stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3277 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3278 """ 3279 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3280 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3281 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3282 See also: `Order()` docstring. 3283 3284 :param lots: volume, integer count of lots >= 1. 3285 :param targetPrice: target price > 0. This is open trade price for limit order. 3286 :return: JSON with response from broker server. 3287 """ 3288 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Sell limit-order (above current price). You must specify only 2 parameters:
lots and target price to open sell limit-order. If you try to create sell limit-order below current price then
broker immediately open Sell market order, such as if you do simple --sell operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3290 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3291 """ 3292 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3293 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3294 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3295 target price value then broker opens a limit order. See also: `Order()` docstring. 3296 3297 :param lots: volume, integer count of lots >= 1. 3298 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3299 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3300 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3301 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3302 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3303 :param expDate: string "Undefined" by default or local date in future. 3304 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3305 This date is converting to UTC format for server. 3306 :return: JSON with response from broker server. 3307 """ 3308 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order.
In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for sell stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3310 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3311 """ 3312 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3313 3314 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3315 :param allOrdersIDs: pre-received lists of all active pending orders. 3316 This avoids unnecessary downloading data from the server. 3317 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3318 """ 3319 if self.accountId is None or not self.accountId: 3320 uLogger.error("Variable `accountId` must be defined for using this method!") 3321 raise Exception("Account ID required") 3322 3323 if orderIDs: 3324 if allOrdersIDs is None or not allOrdersIDs: 3325 rawOrders = self.RequestPendingOrders() 3326 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3327 3328 if allStopOrdersIDs is None or not allStopOrdersIDs: 3329 rawStopOrders = self.RequestStopOrders() 3330 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3331 3332 for orderID in orderIDs: 3333 idInPendingOrders = orderID in allOrdersIDs 3334 idInStopOrders = orderID in allStopOrdersIDs 3335 3336 if not (idInPendingOrders or idInStopOrders): 3337 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3338 continue 3339 3340 else: 3341 if idInPendingOrders: 3342 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3343 3344 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3345 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3346 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3347 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3348 3349 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3350 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3351 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3352 3353 else: 3354 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3355 3356 elif idInStopOrders: 3357 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3358 3359 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3360 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3361 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3362 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3363 3364 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3365 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3366 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3367 3368 else: 3369 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3370 3371 else: 3372 continue
Cancel order or list of orders by its orderId or stopOrderId for current accountId.
Parameters
- orderIDs: list of integers with
orderIdorstopOrderId. - allOrdersIDs: pre-received lists of all active pending orders. This avoids unnecessary downloading data from the server.
- allStopOrdersIDs: pre-received lists of all active stop orders.
3374 def CloseAllOrders(self) -> None: 3375 """ 3376 Gets a list of open pending and stop orders and cancel it all. 3377 """ 3378 rawOrders = self.RequestPendingOrders() 3379 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3380 lenOrders = len(allOrdersIDs) 3381 3382 rawStopOrders = self.RequestStopOrders() 3383 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3384 lenSOrders = len(allStopOrdersIDs) 3385 3386 if lenOrders > 0 or lenSOrders > 0: 3387 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3388 3389 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3390 3391 else: 3392 uLogger.info("Orders not found, nothing to cancel.")
Gets a list of open pending and stop orders and cancel it all.
3394 def CloseAll(self, *args) -> None: 3395 """ 3396 Close all available (not blocked) opened trades and orders. 3397 3398 Also, you can select one or more keywords case-insensitive: 3399 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3400 3401 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3402 """ 3403 overview = self.Overview(show=False) # get all open trades info 3404 3405 if len(args) == 0: 3406 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3407 self.CloseAllOrders() # close all pending and stop orders 3408 3409 for iType in TKS_INSTRUMENTS: 3410 if iType != "Currencies": 3411 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3412 3413 else: 3414 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3415 lowerArgs = [x.lower() for x in args] 3416 3417 if "orders" in lowerArgs: 3418 self.CloseAllOrders() # close all pending and stop orders 3419 3420 for iType in TKS_INSTRUMENTS: 3421 if iType.lower() in lowerArgs and iType != "Currencies": 3422 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies
Close all available (not blocked) opened trades and orders.
Also, you can select one or more keywords case-insensitive:
orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.
Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.
3424 @staticmethod 3425 def ParseOrderParameters(operation, **inputParameters): 3426 """ 3427 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3428 3429 :param operation: string "Buy" or "Sell". 3430 :param inputParameters: this is dict of strings that looks like this 3431 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3432 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3433 "prices" key: one or more prices to open limit-orders 3434 Counts of values in lots and prices lists must be equals! 3435 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3436 """ 3437 # TODO: update order grid work with api v2 3438 pass 3439 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3440 # 3441 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3442 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3443 # raise Exception("Incorrect value") 3444 # 3445 # if "l" in inputParameters.keys(): 3446 # inputParameters["lots"] = inputParameters.pop("l") 3447 # 3448 # if "p" in inputParameters.keys(): 3449 # inputParameters["prices"] = inputParameters.pop("p") 3450 # 3451 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3452 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3453 # raise Exception("Incorrect value") 3454 # 3455 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3456 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3457 # 3458 # if len(lots) != len(prices): 3459 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3460 # raise Exception("Incorrect value") 3461 # 3462 # uLogger.debug("Extracted parameters for orders:") 3463 # uLogger.debug("lots = {}".format(lots)) 3464 # uLogger.debug("prices = {}".format(prices)) 3465 # 3466 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3467 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3468 # uLogger.debug("Order parameters: {}".format(result)) 3469 # 3470 # return result
Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
Parameters
- operation: string "Buy" or "Sell".
- inputParameters: this is dict of strings that looks like this
{"lots": "L_int,...", "prices": "P_float,..."}where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns
list of dictionaries with all lots and prices to open orders that looks like this
[{"lot": lots_1, "price": price_1}, {...}, ...]
3472 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3473 """ 3474 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3475 3476 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3477 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3478 """ 3479 result = False 3480 msg = "Instrument not defined!" 3481 3482 if portfolio is None or not portfolio: 3483 portfolio = self.Overview(show=False) 3484 3485 if self.ticker: 3486 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3487 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3488 3489 for iType in TKS_INSTRUMENTS: 3490 for instrument in portfolio["stat"][iType]: 3491 if instrument["ticker"] == self.ticker: 3492 result = True 3493 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3494 break 3495 3496 elif self.figi: 3497 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3498 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3499 3500 for iType in TKS_INSTRUMENTS: 3501 for instrument in portfolio["stat"][iType]: 3502 if instrument["figi"] == self.figi: 3503 result = True 3504 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3505 break 3506 3507 else: 3508 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3509 3510 uLogger.debug(msg) 3511 3512 return result
Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif portfolio contains open position with given instrument,Falseotherwise.
3514 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3515 """ 3516 Returns instrument is in the user's portfolio if it presents there. 3517 Instrument must be defined by `ticker` (highly priority) or `figi`. 3518 3519 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3520 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3521 """ 3522 result = None 3523 msg = "Instrument not defined!" 3524 3525 if portfolio is None or not portfolio: 3526 portfolio = self.Overview(show=False) 3527 3528 if self.ticker: 3529 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3530 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3531 3532 for iType in TKS_INSTRUMENTS: 3533 for instrument in portfolio["stat"][iType]: 3534 if instrument["ticker"] == self.ticker: 3535 result = instrument 3536 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3537 break 3538 3539 elif self.figi: 3540 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3541 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3542 3543 for iType in TKS_INSTRUMENTS: 3544 for instrument in portfolio["stat"][iType]: 3545 if instrument["figi"] == self.figi: 3546 result = instrument 3547 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3548 break 3549 3550 else: 3551 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3552 3553 uLogger.debug(msg) 3554 3555 return result
Returns instrument is in the user's portfolio if it presents there.
Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
dict with instrument if portfolio contains open position with this instrument,
Noneotherwise.
3557 def RequestLimits(self) -> dict: 3558 """ 3559 Method for obtaining the available funds for withdrawal for current `accountId`. 3560 3561 See also: 3562 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3563 - `OverviewLimits()` method 3564 3565 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3566 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3567 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3568 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3569 """ 3570 if self.accountId is None or not self.accountId: 3571 uLogger.error("Variable `accountId` must be defined for using this method!") 3572 raise Exception("Account ID required") 3573 3574 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3575 3576 self.body = str({"accountId": self.accountId}) 3577 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3578 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3579 3580 uLogger.debug("Records about available funds for withdrawal successfully received") 3581 3582 return rawLimits
Method for obtaining the available funds for withdrawal for current accountId.
See also:
- REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
OverviewLimits()method
Returns
dict with raw data from server that contains free funds for withdrawal. Example of dict:
{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Heremoneyis an array of portfolio currency positions,blockedis an array of blocked currency positions of the portfolio andblockedGuaranteeis locked money under collateral for futures.
3584 def OverviewLimits(self, show: bool = False) -> dict: 3585 """ 3586 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3587 3588 See also: `RequestLimits()`. 3589 3590 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3591 :return: dict with raw parsed data from server and some calculated statistics about it. 3592 """ 3593 if self.accountId is None or not self.accountId: 3594 uLogger.error("Variable `accountId` must be defined for using this method!") 3595 raise Exception("Account ID required") 3596 3597 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3598 3599 view = { 3600 "rawLimits": rawLimits, 3601 "limits": { # parsed data for every currency: 3602 "money": { # this is an array of portfolio currency positions 3603 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3604 }, 3605 "blocked": { # this is an array of blocked currency 3606 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3607 }, 3608 "blockedGuarantee": { # this is locked money under collateral for futures 3609 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3610 }, 3611 }, 3612 } 3613 3614 # --- Prepare text table with limits in human-readable format: 3615 if show: 3616 info = [ 3617 "# Withdrawal limits\n\n", 3618 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3619 "* **Account ID:** [{}]\n".format(self.accountId), 3620 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3621 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3622 ] 3623 3624 for curr in view["limits"]["money"].keys(): 3625 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3626 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3627 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3628 3629 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3630 "[{}]".format(curr), 3631 "{:.2f}".format(view["limits"]["money"][curr]), 3632 "{:.2f}".format(availableMoney), 3633 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3634 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3635 ) 3636 3637 if curr == "rub": 3638 info.insert(5, infoStr) # insert at first position in table and after headers 3639 3640 else: 3641 info.append(infoStr) 3642 3643 infoText = "".join(info) 3644 3645 uLogger.info(infoText) 3646 3647 if self.withdrawalLimitsFile: 3648 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3649 fH.write(infoText) 3650 3651 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3652 3653 return view
Method for parsing and show table with available funds for withdrawal for current accountId.
See also: RequestLimits().
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print withdrawal limits to log.
Returns
dict with raw parsed data from server and some calculated statistics about it.
3655 def RequestAccounts(self) -> dict: 3656 """ 3657 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3658 3659 See also: 3660 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3661 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3662 - `OverviewUserInfo()` method 3663 3664 :return: dict with raw data from server that contains accounts info. Example of dict: 3665 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3666 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3667 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3668 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3669 """ 3670 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3671 3672 self.body = str({}) 3673 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3674 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3675 3676 uLogger.debug("Records about available accounts successfully received") 3677 3678 return rawAccounts
Method for requesting all brokerage accounts (accountIds) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
- What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
OverviewUserInfo()method
Returns
dict with raw data from server that contains accounts info. Example of dict:
{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. IfclosedDate="1970-01-01T00:00:00Z"it means that account is active now.
3680 def RequestUserInfo(self) -> dict: 3681 """ 3682 Method for requesting common user's information. 3683 3684 See also: 3685 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3686 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3687 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3688 - `OverviewUserInfo()` method 3689 3690 :return: dict with raw data from server that contains user's information. Example of dict: 3691 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3692 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3693 """ 3694 uLogger.debug("Requesting common user's information. Wait, please...") 3695 3696 self.body = str({}) 3697 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3698 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3699 3700 uLogger.debug("Records about current user successfully received") 3701 3702 return rawUserInfo
Method for requesting common user's information.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
- What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
- What does
qualified_for_work_withfield mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with OverviewUserInfo()method
Returns
dict with raw data from server that contains user's information. Example of dict:
{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.
3704 def RequestMarginStatus(self, accountId: str = None) -> dict: 3705 """ 3706 Method for requesting margin calculation for defined account ID. 3707 3708 See also: 3709 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3710 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3711 - `OverviewUserInfo()` method 3712 3713 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3714 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3715 Example of responses: 3716 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3717 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3718 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3719 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3720 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3721 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3722 """ 3723 if accountId is None or not accountId: 3724 if self.accountId is None or not self.accountId: 3725 uLogger.error("Variable `accountId` must be defined for using this method!") 3726 raise Exception("Account ID required") 3727 3728 else: 3729 accountId = self.accountId # use `self.accountId` (main ID) by default 3730 3731 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3732 3733 self.body = str({"accountId": accountId}) 3734 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3735 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3736 3737 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3738 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3739 rawMargin = {} 3740 3741 else: 3742 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3743 3744 return rawMargin
Method for requesting margin calculation for defined account ID.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
- What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
OverviewUserInfo()method
Parameters
- accountId: string with numeric account ID. If
None, then used class fieldaccountId.
Returns
dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400:
{"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns:{}. status code 200:{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.
3746 def RequestTariffLimits(self) -> dict: 3747 """ 3748 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3749 3750 See also: 3751 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3752 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3753 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3754 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3755 - `OverviewUserInfo()` method 3756 3757 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3758 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3759 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3760 """ 3761 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3762 3763 self.body = str({}) 3764 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3765 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3766 3767 uLogger.debug("Records with limits of current tariff successfully received") 3768 3769 return rawTariffLimits
Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
- What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
- Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
- Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
OverviewUserInfo()method
Returns
dict with raw data from server that contains limits of current tariff. Example of dict:
{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.
3771 def RequestBondCoupons(self, iJSON: dict) -> dict: 3772 """ 3773 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3774 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 3775 All dates are in UTC timezone. 3776 3777 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3778 Documentation: 3779 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3780 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3781 3782 See also: `ExtendBondsData()`. 3783 3784 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]` 3785 If raw iJSON is not data of bond then server returns an error [400] with message: 3786 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3787 :return: dictionary with bond payment calendar. Response example 3788 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3789 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3790 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3791 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3792 """ 3793 if iJSON["figi"] is None or not iJSON["figi"]: 3794 uLogger.error("FIGI must be defined for using this method!") 3795 raise Exception("FIGI required") 3796 3797 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 3798 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 3799 3800 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 3801 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 3802 self.figi, 3803 startDate, 3804 endDate, 3805 )) 3806 3807 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 3808 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 3809 calendar = self.SendAPIRequest(calendarURL, reqType="POST", debug=False) 3810 3811 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 3812 uLogger.warning("Instrument type is not bond!") 3813 3814 else: 3815 uLogger.debug("Records about bond payment calendar successfully received") 3816 3817 return calendar
Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z".
All dates are in UTC timezone.
REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:
- request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
- response: https://tinkoff.github.io/investAPI/instruments/#coupon
See also: ExtendBondsData().
Parameters
- iJSON: raw json data of a bond from broker server, example
iJSON = self.iList["Bonds"][self.ticker]If raw iJSON is not data of bond then server returns an error [400] with message:{"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns
dictionary with bond payment calendar. Response example
{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}
3819 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 3820 """ 3821 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 3822 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 3823 coupon yields, current yields and some statistics etc. 3824 3825 WARNING! This is too long operation if a lot of bonds requested from broker server. 3826 3827 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 3828 3829 :param instruments: list of strings with tickers or FIGIs. 3830 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 3831 for further used by data scientists or stock analytics. 3832 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 3833 In XLSX-file and Pandas DataFrame fields mean: 3834 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 3835 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 3836 """ 3837 if instruments is None or not instruments: 3838 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3839 raise Exception("Ticker or FIGI required") 3840 3841 if isinstance(instruments, str): 3842 instruments = [instruments] 3843 3844 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3845 3846 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 3847 3848 iCount = len(uniqueInstruments) 3849 tooLong = iCount >= 20 3850 if tooLong: 3851 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 3852 3853 bonds = None 3854 for i, self.figi in enumerate(uniqueInstruments): 3855 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 3856 3857 if "type" in instrument.keys() and instrument["type"] == "Bonds": 3858 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3859 rawBond = self.SearchByFIGI(requestPrice=True) 3860 3861 # Widen raw data with UTC current time (iData["actualDateTime"]): 3862 actualDate = datetime.now(tzutc()) 3863 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 3864 3865 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 3866 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 3867 3868 # Replace some values with human-readable: 3869 iData["nominalCurrency"] = iData["nominal"]["currency"] 3870 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 3871 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 3872 iData["aciCurrency"] = iData["aciValue"]["currency"] 3873 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 3874 iData["issueSize"] = int(iData["issueSize"]) 3875 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 3876 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 3877 iData["step"] = iData["step"] if "step" in iData.keys() else 0 3878 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 3879 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 3880 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 3881 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 3882 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 3883 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 3884 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 3885 3886 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 3887 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 3888 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 3889 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 3890 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 3891 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 3892 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 3893 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 3894 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 3895 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 3896 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 3897 3898 # Widen raw data with calendar data from `rawCalendar` values: 3899 calendarData = [] 3900 for item in iData["rawCalendar"]["events"]: 3901 calendarData.append({ 3902 "couponDate": item["couponDate"], 3903 "couponNumber": int(item["couponNumber"]), 3904 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 3905 "payCurrency": item["payOneBond"]["currency"], 3906 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 3907 "couponType": TKS_COUPON_TYPES[item["couponType"]], 3908 "couponStartDate": item["couponStartDate"], 3909 "couponEndDate": item["couponEndDate"], 3910 "couponPeriod": item["couponPeriod"], 3911 }) 3912 3913 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 3914 if "maturityDate" not in iData.keys(): 3915 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 3916 3917 # Widen raw data with Coupon Rate. 3918 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 3919 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 3920 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 3921 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 3922 3923 # Widen raw data with Yield to Maturity (YTM) on current date. 3924 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 3925 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 3926 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 3927 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 3928 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 3929 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 3930 3931 iData["calendar"] = calendarData # adds calendar at the end 3932 3933 # Remove not used data: 3934 iData.pop("uid") 3935 iData.pop("positionUid") 3936 iData.pop("currentPrice") 3937 iData.pop("rawCalendar") 3938 3939 colNames = list(iData.keys()) 3940 if bonds is None: 3941 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 3942 3943 else: 3944 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 3945 3946 else: 3947 uLogger.warning("Instrument with ticker [{}] and FIGI [{}] is not a bond!".format(instrument["ticker"], instrument["figi"])) 3948 3949 processed = round(100 * (i + 1) / iCount, 1) 3950 if tooLong and processed % 5 == 0: 3951 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 3952 3953 else: 3954 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 3955 3956 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 3957 3958 # Saving bonds from Pandas DataFrame to XLSX sheet: 3959 if xlsx and self.bondsXLSXFile: 3960 with pd.ExcelWriter( 3961 path=self.bondsXLSXFile, 3962 date_format=TKS_DATE_FORMAT, 3963 datetime_format=TKS_DATE_TIME_FORMAT, 3964 mode="w", 3965 ) as writer: 3966 bonds.to_excel( 3967 writer, 3968 sheet_name="Extended bonds data", 3969 index=True, 3970 encoding="UTF-8", 3971 freeze_panes=(1, 1), 3972 ) # saving as XLSX-file with freeze first row and column as headers 3973 3974 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 3975 3976 return bonds
Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().
Parameters
- instruments: list of strings with tickers or FIGIs.
- xlsx: if True then also exports Pandas DataFrame to xlsx-file
bondsXLSXFile, defaultext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns
wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
3978 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 3979 """ 3980 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 3981 3982 WARNING! This is too long operation if a lot of bonds requested from broker server. 3983 3984 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 3985 3986 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 3987 extended information about bonds: main info, current prices, bond payment calendar, 3988 coupon yields, current yields and some statistics etc. 3989 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 3990 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 3991 for further used by data scientists or stock analytics. 3992 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 3993 """ 3994 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 3995 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 3996 3997 uLogger.debug("Generating bond payments calendar data. Wait, please...") 3998 3999 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4000 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4001 calendar = None 4002 for bond in extBonds.iterrows(): 4003 for item in bond[1]["calendar"]: 4004 cData = { 4005 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4006 "couponDate": item["couponDate"], 4007 "figi": bond[1]["figi"], 4008 "ticker": bond[1]["ticker"], 4009 "name": bond[1]["name"], 4010 "couponNumber": item["couponNumber"], 4011 "payOneBond": item["payOneBond"], 4012 "payCurrency": item["payCurrency"], 4013 "couponType": item["couponType"], 4014 "couponPeriod": item["couponPeriod"], 4015 "fixDate": item["fixDate"], 4016 "couponStartDate": item["couponStartDate"], 4017 "couponEndDate": item["couponEndDate"], 4018 } 4019 4020 if calendar is None: 4021 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4022 4023 else: 4024 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4025 4026 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4027 4028 # Saving calendar from Pandas DataFrame to XLSX sheet: 4029 if xlsx: 4030 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4031 4032 with pd.ExcelWriter( 4033 path=xlsxCalendarFile, 4034 date_format=TKS_DATE_FORMAT, 4035 datetime_format=TKS_DATE_TIME_FORMAT, 4036 mode="w", 4037 ) as writer: 4038 humanReadable = calendar.copy(deep=True) 4039 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4040 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4041 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4042 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4043 humanReadable.columns = colNames # human-readable column names 4044 4045 humanReadable.to_excel( 4046 writer, 4047 sheet_name="Bond payments calendar", 4048 index=False, 4049 encoding="UTF-8", 4050 freeze_panes=(1, 2), 4051 ) # saving as XLSX-file with freeze first row and column as headers 4052 4053 del humanReadable # release df in memory 4054 4055 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4056 4057 return calendar
Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowBondsCalendar(), ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - xlsx: if True then also exports Pandas DataFrame to file
calendarFile+".xlsx",calendar.xlsxby default, for further used by data scientists or stock analytics.
Returns
Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4059 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4060 """ 4061 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4062 Also, creates Markdown file with calendar data, `calendar.md` by default. 4063 4064 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4065 4066 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4067 extended information about bonds: main info, current prices, bond payment calendar, 4068 coupon yields, current yields and some statistics etc. 4069 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4070 :param show: if `True` then also printing bonds payment calendar to the console, 4071 otherwise save to file `calendarFile` only. `False` by default. 4072 :return: multilines text in Markdown format with bonds payment calendar as a table. 4073 """ 4074 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4075 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4076 4077 infoText = "# Bond payments calendar\n\n" 4078 4079 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4080 4081 if not calendar.empty: 4082 splitLine = "| | | | | | | | | |\n" 4083 4084 info = [ 4085 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4086 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4087 ] 4088 4089 newMonth = False 4090 notOneBond = calendar["figi"].nunique() > 1 4091 for i, bond in enumerate(calendar.iterrows()): 4092 if newMonth and notOneBond: 4093 info.append(splitLine) 4094 4095 info.append( 4096 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4097 " √" if bond[1]["paid"] else " —", 4098 bond[1]["couponDate"].split("T")[0], 4099 bond[1]["figi"], 4100 bond[1]["ticker"], 4101 bond[1]["couponNumber"], 4102 "{} {}".format( 4103 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4104 bond[1]["payCurrency"], 4105 ), 4106 bond[1]["couponType"], 4107 bond[1]["couponPeriod"], 4108 bond[1]["fixDate"].split("T")[0], 4109 ) 4110 ) 4111 4112 if i < len(calendar.values) - 1: 4113 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4114 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4115 newMonth = False if curDate.month == nextDate.month else True 4116 4117 else: 4118 newMonth = False 4119 4120 infoText += "".join(info) 4121 4122 if show: 4123 uLogger.info("{}".format(infoText)) 4124 4125 if self.calendarFile is not None: 4126 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4127 fH.write(infoText) 4128 4129 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4130 4131 else: 4132 infoText += "No data\n" 4133 4134 return infoText
Show bond payments calendar as a table. One row in input bonds dataframe contains one bond.
Also, creates Markdown file with calendar data, calendar.md by default.
See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - show: if
Truethen also printing bonds payment calendar to the console, otherwise save to filecalendarFileonly.Falseby default.
Returns
multilines text in Markdown format with bonds payment calendar as a table.
4136 def OverviewAccounts(self, show: bool = False) -> dict: 4137 """ 4138 Method for parsing and show simple table with all available user accounts. 4139 4140 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4141 4142 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4143 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4144 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4145 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4146 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4147 "closed": "—", "access": "Full access" }, ...}}` 4148 """ 4149 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4150 4151 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4152 accounts = { 4153 item["id"]: { 4154 "type": TKS_ACCOUNT_TYPES[item["type"]], 4155 "name": item["name"], 4156 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4157 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4158 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4159 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4160 } for item in rawAccounts["accounts"] 4161 } 4162 4163 # Raw and parsed data with some fields replaced in "stat" section: 4164 view = { 4165 "rawAccounts": rawAccounts, 4166 "stat": accounts, 4167 } 4168 4169 # --- Prepare simple text table with only accounts data in human-readable format: 4170 if show: 4171 info = [ 4172 "# User accounts\n\n", 4173 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4174 "| Account ID | Type | Status | Name |\n", 4175 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4176 ] 4177 4178 for account in view["stat"].keys(): 4179 info.extend([ 4180 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4181 account, 4182 view["stat"][account]["type"], 4183 view["stat"][account]["status"], 4184 view["stat"][account]["name"], 4185 ) 4186 ]) 4187 4188 infoText = "".join(info) 4189 4190 uLogger.info(infoText) 4191 4192 if self.userAccountsFile: 4193 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4194 fH.write(infoText) 4195 4196 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4197 4198 return view
Method for parsing and show simple table with all available user accounts.
See also: RequestAccounts() and OverviewUserInfo() methods.
Parameters
- show: if
Falsethen only dictionary with accounts data returns, ifTruethen also print it to log.
Returns
dict with parsed accounts data received from
RequestAccounts()method. Example of dict:view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}
4200 def OverviewUserInfo(self, show: bool = False) -> dict: 4201 """ 4202 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4203 4204 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4205 4206 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4207 :return: dict with raw parsed data from server and some calculated statistics about it. 4208 """ 4209 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4210 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4211 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4212 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4213 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4214 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4215 4216 # This is dict with parsed common user data: 4217 userInfo = { 4218 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4219 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4220 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4221 "tariff": rawUserInfo["tariff"], 4222 } 4223 4224 # This is an array of dict with parsed margin statuses for every account IDs: 4225 margins = {} 4226 for accountId in accounts.keys(): 4227 if rawMargins[accountId]: 4228 margins[accountId] = { 4229 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4230 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4231 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4232 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4233 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4234 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4235 } 4236 4237 else: 4238 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4239 4240 unary = {} # unary-connection limits 4241 for item in rawTariffLimits["unaryLimits"]: 4242 if item["limitPerMinute"] in unary.keys(): 4243 unary[item["limitPerMinute"]].extend(item["methods"]) 4244 4245 else: 4246 unary[item["limitPerMinute"]] = item["methods"] 4247 4248 stream = {} # stream-connection limits 4249 for item in rawTariffLimits["streamLimits"]: 4250 if item["limit"] in stream.keys(): 4251 stream[item["limit"]].extend(item["streams"]) 4252 4253 else: 4254 stream[item["limit"]] = item["streams"] 4255 4256 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4257 limits = { 4258 "unary": unary, 4259 "stream": stream, 4260 } 4261 4262 # Raw and parsed data as an output result: 4263 view = { 4264 "rawUserInfo": rawUserInfo, 4265 "rawAccounts": rawAccounts, 4266 "rawMargins": rawMargins, 4267 "rawTariffLimits": rawTariffLimits, 4268 "stat": { 4269 "userInfo": userInfo, 4270 "accounts": accounts, 4271 "margins": margins, 4272 "limits": limits, 4273 }, 4274 } 4275 4276 # --- Prepare text table with user information in human-readable format: 4277 if show: 4278 info = [ 4279 "# Full user information\n\n", 4280 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4281 "## Common information\n\n", 4282 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4283 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4284 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4285 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4286 "\n## User accounts\n\n", 4287 ] 4288 4289 for account in view["stat"]["accounts"].keys(): 4290 info.extend([ 4291 "### ID: [{}]\n\n".format(account), 4292 "| Parameters | Values |\n", 4293 "|----------------------|--------------------------------------------------------------|\n", 4294 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4295 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4296 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4297 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4298 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4299 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4300 ]) 4301 4302 if margins[account]: 4303 info.extend([ 4304 "| Margin status: | Enabled |\n", 4305 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4306 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4307 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4308 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4309 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4310 ]) 4311 4312 else: 4313 info.append("| Margin status: | Disabled |\n\n") 4314 4315 info.extend([ 4316 "\n## Current user tariff limits\n", 4317 "\nSee also:\n", 4318 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4319 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4320 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4321 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4322 "\n### Unary limits\n", 4323 ]) 4324 4325 if unary: 4326 for key, values in sorted(unary.items()): 4327 info.append("\n* Max requests per minute: {}\n".format(key)) 4328 4329 for value in values: 4330 info.append(" - {}\n".format(value)) 4331 4332 else: 4333 info.append("\nNot available\n") 4334 4335 info.append("\n### Stream limits\n") 4336 4337 if stream: 4338 for key, values in sorted(stream.items()): 4339 info.append("\n* Max stream connections: {}\n".format(key)) 4340 4341 for value in values: 4342 info.append(" - {}\n".format(value)) 4343 4344 else: 4345 info.append("\nNot available\n") 4346 4347 infoText = "".join(info) 4348 4349 uLogger.info(infoText) 4350 4351 if self.userInfoFile: 4352 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4353 fH.write(infoText) 4354 4355 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4356 4357 return view
Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).
See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print user's data to log.
Returns
dict with raw parsed data from server and some calculated statistics about it.
4360class Args: 4361 """ 4362 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4363 """ 4364 def __init__(self, **kwargs): 4365 self.__dict__.update(kwargs) 4366 4367 def __getattr__(self, item): 4368 return None
If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.
4371def ParseArgs(): 4372 """This function get and parse command line keys.""" 4373 parser = ArgumentParser() # command-line string parser 4374 4375 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4376 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4377 4378 # --- options: 4379 4380 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4381 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4382 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4383 4384 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4385 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4386 4387 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4388 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4389 4390 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4391 4392 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4393 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4394 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4395 4396 parser.add_argument("--debug-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4397 4398 # --- commands: 4399 4400 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4401 4402 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4403 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4404 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4405 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4406 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4407 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4408 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4409 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4410 4411 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4412 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4413 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4414 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4415 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4416 4417 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4418 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4419 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4420 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4421 4422 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4423 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4424 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4425 4426 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4427 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4428 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4429 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4430 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4431 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4432 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4433 4434 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4435 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4436 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` key, including for currencies tickers.") 4437 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers, including for currencies tickers.") 4438 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.") 4439 4440 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4441 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4442 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4443 4444 cmdArgs = parser.parse_args() 4445 return cmdArgs
This function get and parse command line keys.
4448def Main(**kwargs): 4449 """ 4450 Main function for work with TKSBrokerAPI in the console. 4451 4452 See examples: 4453 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4454 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4455 """ 4456 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4457 4458 if args.debug_level: 4459 uLogger.level = 10 # always debug level by default 4460 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4461 4462 exitCode = 0 4463 start = datetime.now(tzutc()) 4464 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4465 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4466 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4467 )) 4468 4469 # trying to calculate full current version: 4470 buildVersion = __version__ 4471 try: 4472 v = version("tksbrokerapi") 4473 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4474 4475 except Exception: 4476 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4477 4478 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4479 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4480 4481 try: 4482 if args.version: 4483 print("TKSBrokerAPI {}".format(buildVersion)) 4484 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4485 4486 else: 4487 # Init class for trading with Tinkoff Broker: TODO: rename `server` to `trader` 4488 server = TinkoffBrokerServer( 4489 token=args.token, 4490 accountId=args.account_id, 4491 useCache=not args.no_cache, 4492 ) 4493 4494 # --- set some options: 4495 4496 if args.ticker: 4497 if args.ticker in server.aliasesKeys: 4498 server.ticker = server.aliases[args.ticker] # Replace some tickers with its aliases 4499 4500 else: 4501 server.ticker = args.ticker 4502 4503 if args.figi: 4504 server.figi = args.figi 4505 4506 if args.depth is not None: 4507 server.depth = args.depth 4508 4509 # --- do one of commands: 4510 4511 if args.list: 4512 if args.output is not None: 4513 server.instrumentsFile = args.output 4514 4515 server.ShowInstrumentsInfo(show=True) 4516 4517 elif args.list_xlsx: 4518 server.DumpInstrumentsAsXLSX(forceUpdate=False) 4519 4520 elif args.bonds_xlsx is not None: 4521 if args.output is not None: 4522 server.bondsXLSXFile = args.output 4523 4524 if len(args.bonds_xlsx) == 0: 4525 server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4526 4527 else: 4528 server.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4529 4530 elif args.search: 4531 if args.output is not None: 4532 server.searchResultsFile = args.output 4533 4534 server.SearchInstruments(pattern=args.search[0], show=True) 4535 4536 elif args.info: 4537 if not (args.ticker or args.figi): 4538 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4539 raise Exception("Ticker or FIGI required") 4540 4541 if args.output is not None: 4542 server.infoFile = args.output 4543 4544 if args.ticker: 4545 server.SearchByTicker(requestPrice=True, show=True, debug=False) # show info and current prices by ticker name 4546 4547 else: 4548 server.SearchByFIGI(requestPrice=True, show=True, debug=False) # show info and current prices by FIGI id 4549 4550 elif args.calendar is not None: 4551 if args.output is not None: 4552 server.calendarFile = args.output 4553 4554 if len(args.calendar) == 0: 4555 bondsData = server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4556 4557 else: 4558 bondsData = server.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4559 4560 server.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4561 4562 elif args.price: 4563 if not (args.ticker or args.figi): 4564 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4565 raise Exception("Ticker or FIGI required") 4566 4567 server.GetCurrentPrices(show=True) 4568 4569 elif args.prices is not None: 4570 if args.output is not None: 4571 server.pricesFile = args.output 4572 4573 server.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4574 4575 elif args.overview: 4576 if args.output is not None: 4577 server.overviewFile = args.output 4578 4579 server.Overview(show=True, details="full") 4580 4581 elif args.overview_digest: 4582 if args.output is not None: 4583 server.overviewDigestFile = args.output 4584 4585 server.Overview(show=True, details="digest") 4586 4587 elif args.overview_positions: 4588 if args.output is not None: 4589 server.overviewPositionsFile = args.output 4590 4591 server.Overview(show=True, details="positions") 4592 4593 elif args.overview_orders: 4594 if args.output is not None: 4595 server.overviewOrdersFile = args.output 4596 4597 server.Overview(show=True, details="orders") 4598 4599 elif args.overview_analytics: 4600 if args.output is not None: 4601 server.overviewAnalyticsFile = args.output 4602 4603 server.Overview(show=True, details="analytics") 4604 4605 elif args.deals is not None: 4606 if args.output is not None: 4607 server.reportFile = args.output 4608 4609 if 0 <= len(args.deals) < 3: 4610 server.Deals( 4611 start=args.deals[0] if len(args.deals) >= 1 else None, 4612 end=args.deals[1] if len(args.deals) == 2 else None, 4613 show=True, # Always show deals report in console 4614 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 4615 ) 4616 4617 else: 4618 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4619 raise Exception("Incorrect value") 4620 4621 elif args.history is not None: 4622 if args.output is not None: 4623 server.historyFile = args.output 4624 4625 if 0 <= len(args.history) < 3: 4626 dataReceived = server.History( 4627 start=args.history[0] if len(args.history) >= 1 else None, 4628 end=args.history[1] if len(args.history) == 2 else None, 4629 interval="hour" if args.interval is None or not args.interval else args.interval, 4630 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 4631 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 4632 show=True, # shows all downloaded candles in console 4633 ) 4634 4635 if args.render_chart is not None and dataReceived is not None: 4636 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4637 4638 server.ShowHistoryChart( 4639 candles=dataReceived, 4640 interact=iChart, 4641 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4642 ) 4643 4644 else: 4645 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4646 raise Exception("Incorrect value") 4647 4648 elif args.load_history is not None: 4649 histData = server.LoadHistory(filePath=args.load_history) # load data from file and show history in console 4650 4651 if args.render_chart is not None and histData is not None: 4652 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4653 server.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 4654 4655 server.ShowHistoryChart( 4656 candles=histData, 4657 interact=iChart, 4658 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4659 ) 4660 4661 elif args.trade is not None: 4662 if 1 <= len(args.trade) <= 5: 4663 server.Trade( 4664 operation=args.trade[0], 4665 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 4666 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 4667 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 4668 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 4669 ) 4670 4671 else: 4672 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4673 4674 elif args.buy is not None: 4675 if 0 <= len(args.buy) <= 4: 4676 server.Buy( 4677 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 4678 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 4679 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 4680 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 4681 ) 4682 4683 else: 4684 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4685 4686 elif args.sell is not None: 4687 if 0 <= len(args.sell) <= 4: 4688 server.Sell( 4689 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 4690 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 4691 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 4692 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 4693 ) 4694 4695 else: 4696 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4697 4698 elif args.order: 4699 if 4 <= len(args.order) <= 7: 4700 server.Order( 4701 operation=args.order[0], 4702 orderType=args.order[1], 4703 lots=int(args.order[2]), 4704 targetPrice=float(args.order[3]), 4705 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 4706 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 4707 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 4708 ) 4709 4710 else: 4711 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 4712 4713 elif args.buy_limit: 4714 server.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 4715 4716 elif args.sell_limit: 4717 server.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 4718 4719 elif args.buy_stop: 4720 if 2 <= len(args.buy_stop) <= 7: 4721 server.BuyStop( 4722 lots=int(args.buy_stop[0]), 4723 targetPrice=float(args.buy_stop[1]), 4724 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 4725 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 4726 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 4727 ) 4728 4729 else: 4730 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4731 4732 elif args.sell_stop: 4733 if 2 <= len(args.sell_stop) <= 7: 4734 server.SellStop( 4735 lots=int(args.sell_stop[0]), 4736 targetPrice=float(args.sell_stop[1]), 4737 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 4738 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 4739 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 4740 ) 4741 4742 else: 4743 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 4744 4745 # elif args.buy_order_grid is not None: 4746 # # update order grid work with api v2 4747 # if len(args.buy_order_grid) == 2: 4748 # orderParams = server.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 4749 # 4750 # for order in orderParams: 4751 # server.Order(operation="Buy", lots=order["lot"], price=order["price"]) 4752 # 4753 # else: 4754 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4755 # 4756 # elif args.sell_order_grid is not None: 4757 # # update order grid work with api v2 4758 # if len(args.sell_order_grid) >= 2: 4759 # orderParams = server.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 4760 # 4761 # for order in orderParams: 4762 # server.Order(operation="Sell", lots=order["lot"], price=order["price"]) 4763 # 4764 # else: 4765 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4766 4767 elif args.close_order is not None: 4768 server.CloseOrders(args.close_order) # close only one order 4769 4770 elif args.close_orders is not None: 4771 server.CloseOrders(args.close_orders) # close list of orders 4772 4773 elif args.close_trade: 4774 if not args.ticker: 4775 uLogger.error("`--ticker` key is required for this operation!") 4776 raise Exception("Ticker required") 4777 4778 server.CloseTrades([args.ticker]) # close only one trade 4779 4780 elif args.close_trades is not None: 4781 server.CloseTrades(args.close_trades) # close trades for list of tickers 4782 4783 elif args.close_all is not None: 4784 server.CloseAll(*args.close_all) 4785 4786 elif args.limits: 4787 if args.output is not None: 4788 server.withdrawalLimitsFile = args.output 4789 4790 server.OverviewLimits(show=True) 4791 4792 elif args.user_info: 4793 if args.output is not None: 4794 server.userInfoFile = args.output 4795 4796 server.OverviewUserInfo(show=True) 4797 4798 elif args.account: 4799 if args.output is not None: 4800 server.userAccountsFile = args.output 4801 4802 server.OverviewAccounts(show=True) 4803 4804 else: 4805 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 4806 raise Exception("There is no command to execute") 4807 4808 except Exception: 4809 trace = tb.format_exc() 4810 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 4811 if e in trace: 4812 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 4813 break 4814 4815 uLogger.debug(trace) 4816 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 4817 exitCode = 255 # an error occurred, must be open a ticket for this issue 4818 4819 finally: 4820 finish = datetime.now(tzutc()) 4821 4822 if exitCode == 0: 4823 uLogger.debug("All operations were finished success (summary code is 0).") 4824 4825 else: 4826 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 4827 os.path.abspath(uLog.defaultLogFile), exitCode, 4828 )) 4829 4830 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 4831 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 4832 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4833 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4834 )) 4835 4836 if not kwargs: 4837 sys.exit(exitCode) 4838 4839 else: 4840 return exitCode
Main function for work with TKSBrokerAPI in the console.
See examples: